mirror of
https://github.com/home-assistant/core.git
synced 2025-09-20 02:19:36 +00:00
Compare commits
154 Commits
fix-host-d
...
2023.11.2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a3319262ac | ||
![]() |
eaf711335d | ||
![]() |
f120558750 | ||
![]() |
30dc05cdd7 | ||
![]() |
8ce746972f | ||
![]() |
f946ed9e16 | ||
![]() |
0ffc1bae76 | ||
![]() |
d1a3a5895b | ||
![]() |
f9c70fd3c8 | ||
![]() |
70f0ee81c9 | ||
![]() |
95d4254074 | ||
![]() |
c8d3e377f0 | ||
![]() |
da1c282c1b | ||
![]() |
35c0c9958d | ||
![]() |
93a0bd351a | ||
![]() |
dbdd9d74cf | ||
![]() |
3cac87cf30 | ||
![]() |
d019045199 | ||
![]() |
8f684ab102 | ||
![]() |
c17def27fc | ||
![]() |
27d8d1011e | ||
![]() |
e2270a305d | ||
![]() |
6fd8973a00 | ||
![]() |
9a37868244 | ||
![]() |
9327c51115 | ||
![]() |
e56e75114a | ||
![]() |
f45114371e | ||
![]() |
7e2c12b0a9 | ||
![]() |
050f1085d0 | ||
![]() |
334a02bc2b | ||
![]() |
412fa4c65a | ||
![]() |
2b36befe95 | ||
![]() |
aa623cc15c | ||
![]() |
b0bb91ec08 | ||
![]() |
ce12d82624 | ||
![]() |
9eff9ee374 | ||
![]() |
1ef460cffe | ||
![]() |
42243f1433 | ||
![]() |
8a07c10d88 | ||
![]() |
730a3f7870 | ||
![]() |
718901d2ad | ||
![]() |
d95d4d0184 | ||
![]() |
67ce51899f | ||
![]() |
810681b357 | ||
![]() |
0b0f099d27 | ||
![]() |
4a56d0ec1d | ||
![]() |
910654bf78 | ||
![]() |
1a823376d8 | ||
![]() |
ba634ac346 | ||
![]() |
92486b1ff0 | ||
![]() |
06d26b7c7f | ||
![]() |
1dcd66d75c | ||
![]() |
c811e0db49 | ||
![]() |
dc30ddc24b | ||
![]() |
239fa04d02 | ||
![]() |
2be229c5b5 | ||
![]() |
5b4df0f7ff | ||
![]() |
355b51d4c8 | ||
![]() |
0c8074bab4 | ||
![]() |
acd98e9b40 | ||
![]() |
0b8d4235c3 | ||
![]() |
4ce859b4e4 | ||
![]() |
18acec32b8 | ||
![]() |
cfa2f2ce61 | ||
![]() |
aa5ea5ebc3 | ||
![]() |
bcea021c14 | ||
![]() |
ea2d2ba7b7 | ||
![]() |
c5f21fefbe | ||
![]() |
9910f9e0ae | ||
![]() |
f0a06efa1f | ||
![]() |
8992d15ffc | ||
![]() |
e097dc02dd | ||
![]() |
bfae1468d6 | ||
![]() |
09ed6e9f9b | ||
![]() |
040ecb74e0 | ||
![]() |
a48e63aa28 | ||
![]() |
19479b2a68 | ||
![]() |
9ae29e243d | ||
![]() |
e309bd764b | ||
![]() |
777ffe6946 | ||
![]() |
fa0f679a9a | ||
![]() |
26b7e94c4f | ||
![]() |
957998ea8d | ||
![]() |
abaeacbd6b | ||
![]() |
d76c16fa3a | ||
![]() |
67edb98e59 | ||
![]() |
376a79eb42 | ||
![]() |
41500cbe9b | ||
![]() |
06f27e7e74 | ||
![]() |
a3ebfaebe7 | ||
![]() |
8d781ff063 | ||
![]() |
bac39f0061 | ||
![]() |
c7b702f3c2 | ||
![]() |
3728f3da69 | ||
![]() |
31d8f4b35d | ||
![]() |
f113d9aa71 | ||
![]() |
891ad0b1be | ||
![]() |
5c16a8247a | ||
![]() |
483671bf9f | ||
![]() |
6f73d2aac5 | ||
![]() |
f5b3661836 | ||
![]() |
f70c13214c | ||
![]() |
70e8978123 | ||
![]() |
031b1c26ce | ||
![]() |
13580a334f | ||
![]() |
e81bfb959e | ||
![]() |
fefe930506 | ||
![]() |
5ac7e8b1ac | ||
![]() |
36512f7157 | ||
![]() |
cc3ae9e103 | ||
![]() |
12482216f6 | ||
![]() |
20409d0124 | ||
![]() |
a741bc9951 | ||
![]() |
59d2bce369 | ||
![]() |
eef318f63c | ||
![]() |
9c8a4bb4eb | ||
![]() |
9c9f1ea685 | ||
![]() |
85d999b020 | ||
![]() |
bcddf52364 | ||
![]() |
07e4e1379a | ||
![]() |
f9f010643a | ||
![]() |
974c34e2b6 | ||
![]() |
1c3de76b04 | ||
![]() |
bee63ca654 | ||
![]() |
29c99f419f | ||
![]() |
3d321c5ca7 | ||
![]() |
4617c16a96 | ||
![]() |
a60656bf29 | ||
![]() |
2eb2a65197 | ||
![]() |
867aaf10ee | ||
![]() |
7fe1ac901f | ||
![]() |
5dca3844ef | ||
![]() |
b5c75a2f2f | ||
![]() |
62fc9dfd6c | ||
![]() |
0573981d6f | ||
![]() |
cc7a4d01e3 | ||
![]() |
293025ab6c | ||
![]() |
a490b5e286 | ||
![]() |
7e4da1d03b | ||
![]() |
9e140864eb | ||
![]() |
a6f88fb123 | ||
![]() |
386c5ecc3e | ||
![]() |
0d7fb5b026 | ||
![]() |
767b7ba4d6 | ||
![]() |
f2cef7245a | ||
![]() |
701a5d7758 | ||
![]() |
244fccdae6 | ||
![]() |
10e6a26717 | ||
![]() |
5fe5013198 | ||
![]() |
0a0584b053 | ||
![]() |
62733e830f | ||
![]() |
bbcfb5f30e | ||
![]() |
5b0e0b07b3 | ||
![]() |
05fd64fe80 |
@@ -45,6 +45,7 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/switch/**
|
||||
- homeassistant/components/text/**
|
||||
- homeassistant/components/time/**
|
||||
- homeassistant/components/todo/**
|
||||
- homeassistant/components/tts/**
|
||||
- homeassistant/components/update/**
|
||||
- homeassistant/components/vacuum/**
|
||||
|
@@ -19,6 +19,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED
|
||||
|
||||
# These are events that do not contain any sensitive data
|
||||
# Except for state_changed, which is handled accordingly.
|
||||
@@ -28,6 +29,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[str]] = {
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
EVENT_DEVICE_REGISTRY_UPDATED,
|
||||
EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED,
|
||||
EVENT_LOVELACE_UPDATED,
|
||||
EVENT_PANELS_UPDATED,
|
||||
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
||||
|
@@ -9,5 +9,5 @@
|
||||
},
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["jaraco.abode", "lomond"],
|
||||
"requirements": ["jaraco.abode==3.3.0"]
|
||||
"requirements": ["jaraco.abode==3.3.0", "jaraco.functools==3.9.0"]
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.3.0"]
|
||||
"requirements": ["aioairzone-cloud==0.3.5"]
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"loggers": ["adb_shell", "androidtv", "pure_python_adb"],
|
||||
"requirements": [
|
||||
"adb-shell[async]==0.4.4",
|
||||
"androidtv[async]==0.0.72",
|
||||
"androidtv[async]==0.0.73",
|
||||
"pure-python-adb[async]==0.3.0.dev0"
|
||||
]
|
||||
}
|
||||
|
@@ -21,6 +21,15 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
COMMAND_TO_ATTRIBUTE = {
|
||||
"wakeup": ("power", "turn_on"),
|
||||
"suspend": ("power", "turn_off"),
|
||||
"turn_on": ("power", "turn_on"),
|
||||
"turn_off": ("power", "turn_off"),
|
||||
"volume_up": ("audio", "volume_up"),
|
||||
"volume_down": ("audio", "volume_down"),
|
||||
"home_hold": ("remote_control", "home"),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -61,7 +70,13 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity):
|
||||
|
||||
for _ in range(num_repeats):
|
||||
for single_command in command:
|
||||
attr_value = getattr(self.atv.remote_control, single_command, None)
|
||||
attr_value = None
|
||||
if attributes := COMMAND_TO_ATTRIBUTE.get(single_command):
|
||||
attr_value = self.atv
|
||||
for attr_name in attributes:
|
||||
attr_value = getattr(attr_value, attr_name, None)
|
||||
if not attr_value:
|
||||
attr_value = getattr(self.atv.remote_control, single_command, None)
|
||||
if not attr_value:
|
||||
raise ValueError("Command not found. Exiting sequence")
|
||||
|
||||
|
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==1.10.0", "yalexs-ble==2.3.1"]
|
||||
"requirements": ["yalexs==1.10.0", "yalexs-ble==2.3.2"]
|
||||
}
|
||||
|
@@ -86,8 +86,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
blink.auth = Auth(auth_data, no_prompt=True, session=session)
|
||||
blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
coordinator = BlinkUpdateCoordinator(hass, blink)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
try:
|
||||
await blink.start()
|
||||
@@ -101,6 +99,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if not blink.available:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/blink",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["blinkpy"],
|
||||
"requirements": ["blinkpy==0.22.2"]
|
||||
"requirements": ["blinkpy==0.22.3"]
|
||||
}
|
||||
|
@@ -330,7 +330,7 @@ class BaseHaRemoteScanner(BaseHaScanner):
|
||||
prev_manufacturer_data = prev_advertisement.manufacturer_data
|
||||
prev_name = prev_device.name
|
||||
|
||||
if local_name and prev_name and len(prev_name) > len(local_name):
|
||||
if prev_name and (not local_name or len(prev_name) > len(local_name)):
|
||||
local_name = prev_name
|
||||
|
||||
if service_uuids and service_uuids != prev_service_uuids:
|
||||
|
@@ -15,10 +15,10 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==0.21.1",
|
||||
"bleak-retry-connector==3.2.1",
|
||||
"bleak-retry-connector==3.3.0",
|
||||
"bluetooth-adapters==0.16.1",
|
||||
"bluetooth-auto-recovery==1.2.3",
|
||||
"bluetooth-data-tools==1.13.0",
|
||||
"bluetooth-data-tools==1.14.0",
|
||||
"dbus-fast==2.12.0"
|
||||
]
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Generic, TypedDict, TypeVar, cast
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
ATTR_CONNECTIONS,
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_NAME,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
@@ -16,7 +17,7 @@ from homeassistant.const import (
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_platform import async_get_current_platform
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
@@ -644,6 +645,8 @@ class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProce
|
||||
self._attr_unique_id = f"{address}-{key}"
|
||||
if ATTR_NAME not in self._attr_device_info:
|
||||
self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name
|
||||
if device_id is None:
|
||||
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_BLUETOOTH, address)}
|
||||
self._attr_name = processor.entity_names.get(entity_key)
|
||||
|
||||
@property
|
||||
|
@@ -5,9 +5,9 @@ from typing import cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, Platform
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import DeviceAutomationType, async_get_device_automation_platform
|
||||
@@ -55,31 +55,42 @@ async def async_validate_device_automation_config(
|
||||
platform = await async_get_device_automation_platform(
|
||||
hass, validated_config[CONF_DOMAIN], automation_type
|
||||
)
|
||||
|
||||
# Make sure the referenced device and optional entity exist
|
||||
device_registry = dr.async_get(hass)
|
||||
if not (device := device_registry.async_get(validated_config[CONF_DEVICE_ID])):
|
||||
# The device referenced by the device automation does not exist
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Unknown device '{validated_config[CONF_DEVICE_ID]}'"
|
||||
)
|
||||
if entity_id := validated_config.get(CONF_ENTITY_ID):
|
||||
try:
|
||||
er.async_validate_entity_id(er.async_get(hass), entity_id)
|
||||
except vol.Invalid as err:
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Unknown entity '{entity_id}'"
|
||||
) from err
|
||||
|
||||
if not hasattr(platform, DYNAMIC_VALIDATOR[automation_type]):
|
||||
# Pass the unvalidated config to avoid mutating the raw config twice
|
||||
return cast(
|
||||
ConfigType, getattr(platform, STATIC_VALIDATOR[automation_type])(config)
|
||||
)
|
||||
|
||||
# Bypass checks for entity platforms
|
||||
# Devices are not linked to config entries from entity platform domains, skip
|
||||
# the checks below which look for a config entry matching the device automation
|
||||
# domain
|
||||
if (
|
||||
automation_type == DeviceAutomationType.ACTION
|
||||
and validated_config[CONF_DOMAIN] in ENTITY_PLATFORMS
|
||||
):
|
||||
# Pass the unvalidated config to avoid mutating the raw config twice
|
||||
return cast(
|
||||
ConfigType,
|
||||
await getattr(platform, DYNAMIC_VALIDATOR[automation_type])(hass, config),
|
||||
)
|
||||
|
||||
# Only call the dynamic validator if the referenced device exists and the relevant
|
||||
# config entry is loaded
|
||||
registry = dr.async_get(hass)
|
||||
if not (device := registry.async_get(validated_config[CONF_DEVICE_ID])):
|
||||
# The device referenced by the device automation does not exist
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Unknown device '{validated_config[CONF_DEVICE_ID]}'"
|
||||
)
|
||||
|
||||
# Find a config entry with the same domain as the device automation
|
||||
device_config_entry = None
|
||||
for entry_id in device.config_entries:
|
||||
if (
|
||||
@@ -91,7 +102,7 @@ async def async_validate_device_automation_config(
|
||||
break
|
||||
|
||||
if not device_config_entry:
|
||||
# The config entry referenced by the device automation does not exist
|
||||
# There's no config entry with the same domain as the device automation
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Device '{validated_config[CONF_DEVICE_ID]}' has no config entry from "
|
||||
f"domain '{validated_config[CONF_DOMAIN]}'"
|
||||
|
@@ -53,6 +53,8 @@ class DSMRConnection:
|
||||
self._protocol = protocol
|
||||
self._telegram: dict[str, DSMRObject] = {}
|
||||
self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER
|
||||
if dsmr_version == "5B":
|
||||
self._equipment_identifier = obis_ref.BELGIUM_EQUIPMENT_IDENTIFIER
|
||||
if dsmr_version == "5L":
|
||||
self._equipment_identifier = obis_ref.LUXEMBOURG_EQUIPMENT_IDENTIFIER
|
||||
if dsmr_version == "Q3D":
|
||||
|
@@ -34,6 +34,3 @@ DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"}
|
||||
|
||||
DSMR_PROTOCOL = "dsmr_protocol"
|
||||
RFXTRX_DSMR_PROTOCOL = "rfxtrx_dsmr_protocol"
|
||||
|
||||
# Temp obis until sensors replaced by mbus variants
|
||||
BELGIUM_5MIN_GAS_METER_READING = r"\d-\d:24\.2\.3.+?\r\n"
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["dsmr_parser"],
|
||||
"requirements": ["dsmr-parser==1.3.0"]
|
||||
"requirements": ["dsmr-parser==1.3.1"]
|
||||
}
|
||||
|
@@ -44,7 +44,6 @@ from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import (
|
||||
BELGIUM_5MIN_GAS_METER_READING,
|
||||
CONF_DSMR_VERSION,
|
||||
CONF_PRECISION,
|
||||
CONF_PROTOCOL,
|
||||
@@ -382,16 +381,6 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
DSMRSensorEntityDescription(
|
||||
key="belgium_5min_gas_meter_reading",
|
||||
translation_key="gas_meter_reading",
|
||||
obis_reference=BELGIUM_5MIN_GAS_METER_READING,
|
||||
dsmr_versions={"5B"},
|
||||
is_gas=True,
|
||||
force_update=True,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
DSMRSensorEntityDescription(
|
||||
key="gas_meter_reading",
|
||||
translation_key="gas_meter_reading",
|
||||
@@ -405,6 +394,31 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
)
|
||||
|
||||
|
||||
def add_gas_sensor_5B(telegram: dict[str, DSMRObject]) -> DSMRSensorEntityDescription:
|
||||
"""Return correct entity for 5B Gas meter."""
|
||||
ref = None
|
||||
if obis_references.BELGIUM_MBUS1_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS1_METER_READING2
|
||||
elif obis_references.BELGIUM_MBUS2_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS2_METER_READING2
|
||||
elif obis_references.BELGIUM_MBUS3_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS3_METER_READING2
|
||||
elif obis_references.BELGIUM_MBUS4_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS4_METER_READING2
|
||||
elif ref is None:
|
||||
ref = obis_references.BELGIUM_MBUS1_METER_READING2
|
||||
return DSMRSensorEntityDescription(
|
||||
key="belgium_5min_gas_meter_reading",
|
||||
translation_key="gas_meter_reading",
|
||||
obis_reference=ref,
|
||||
dsmr_versions={"5B"},
|
||||
is_gas=True,
|
||||
force_update=True,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
@@ -438,6 +452,10 @@ async def async_setup_entry(
|
||||
return (entity_description.device_class, UNIT_CONVERSION[uom])
|
||||
return (entity_description.device_class, uom)
|
||||
|
||||
all_sensors = SENSORS
|
||||
if dsmr_version == "5B":
|
||||
all_sensors += (add_gas_sensor_5B(telegram),)
|
||||
|
||||
entities.extend(
|
||||
[
|
||||
DSMREntity(
|
||||
@@ -448,7 +466,7 @@ async def async_setup_entry(
|
||||
telegram, description
|
||||
), # type: ignore[arg-type]
|
||||
)
|
||||
for description in SENSORS
|
||||
for description in all_sensors
|
||||
if (
|
||||
description.dsmr_versions is None
|
||||
or dsmr_version in description.dsmr_versions
|
||||
|
@@ -141,6 +141,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
translation_key="gas_meter_usage",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:fire",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
@@ -283,6 +284,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
key="dsmr/day-consumption/gas",
|
||||
translation_key="daily_gas_usage",
|
||||
icon="mdi:counter",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
@@ -460,6 +462,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
key="dsmr/current-month/gas",
|
||||
translation_key="current_month_gas_usage",
|
||||
icon="mdi:counter",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
@@ -538,6 +541,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
key="dsmr/current-year/gas",
|
||||
translation_key="current_year_gas_usage",
|
||||
icon="mdi:counter",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
|
@@ -5,5 +5,6 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecoforest",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyecoforest"],
|
||||
"requirements": ["pyecoforest==0.3.0"]
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/econet",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["paho_mqtt", "pyeconet"],
|
||||
"requirements": ["pyeconet==0.1.20"]
|
||||
"requirements": ["pyeconet==0.1.22"]
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"requirements": ["pyenphase==1.13.1"],
|
||||
"requirements": ["pyenphase==1.14.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
@@ -596,6 +596,10 @@ def _async_setup_device_registry(
|
||||
model = project_name[1]
|
||||
hw_version = device_info.project_version
|
||||
|
||||
suggested_area = None
|
||||
if device_info.suggested_area:
|
||||
suggested_area = device_info.suggested_area
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
@@ -606,6 +610,7 @@ def _async_setup_device_registry(
|
||||
model=model,
|
||||
sw_version=sw_version,
|
||||
hw_version=hw_version,
|
||||
suggested_area=suggested_area,
|
||||
)
|
||||
return device_entry.id
|
||||
|
||||
|
@@ -16,8 +16,8 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol"],
|
||||
"requirements": [
|
||||
"async-interrupt==1.1.1",
|
||||
"aioesphomeapi==18.1.0",
|
||||
"bluetooth-data-tools==1.13.0",
|
||||
"aioesphomeapi==18.2.4",
|
||||
"bluetooth-data-tools==1.14.0",
|
||||
"esphome-dashboard-api==1.2.3"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
|
@@ -487,6 +487,18 @@ class EvoBroker:
|
||||
)
|
||||
self.temps = None # these are now stale, will fall back to v2 temps
|
||||
|
||||
except KeyError as err:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Unable to obtain high-precision temperatures. "
|
||||
"It appears the JSON schema is not as expected, "
|
||||
"so the high-precision feature will be disabled until next restart."
|
||||
"Message is: %s"
|
||||
),
|
||||
err,
|
||||
)
|
||||
self.client_v1 = self.temps = None
|
||||
|
||||
else:
|
||||
if (
|
||||
str(self.client_v1.location_id)
|
||||
@@ -495,7 +507,7 @@ class EvoBroker:
|
||||
_LOGGER.warning(
|
||||
"The v2 API's configured location doesn't match "
|
||||
"the v1 API's default location (there is more than one location), "
|
||||
"so the high-precision feature will be disabled"
|
||||
"so the high-precision feature will be disabled until next restart"
|
||||
)
|
||||
self.client_v1 = self.temps = None
|
||||
else:
|
||||
|
@@ -3,14 +3,24 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_automation import toggle_entity
|
||||
from homeassistant.components.device_automation import (
|
||||
async_validate_entity_schema,
|
||||
toggle_entity,
|
||||
)
|
||||
from homeassistant.const import CONF_DOMAIN
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN})
|
||||
_ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN})
|
||||
|
||||
|
||||
async def async_validate_action_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return async_validate_entity_schema(hass, config, _ACTION_SCHEMA)
|
||||
|
||||
|
||||
async def async_get_actions(
|
||||
|
@@ -59,13 +59,16 @@ class FitbitOAuth2Implementation(AuthImplementation):
|
||||
resp = await session.post(self.token_url, data=data, headers=self._headers)
|
||||
resp.raise_for_status()
|
||||
except aiohttp.ClientResponseError as err:
|
||||
error_body = await resp.text()
|
||||
_LOGGER.debug("Client response error body: %s", error_body)
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
error_body = await resp.text() if not session.closed else ""
|
||||
_LOGGER.debug(
|
||||
"Client response error status=%s, body=%s", err.status, error_body
|
||||
)
|
||||
if err.status == HTTPStatus.UNAUTHORIZED:
|
||||
raise FitbitAuthException from err
|
||||
raise FitbitApiException from err
|
||||
raise FitbitAuthException(f"Unauthorized error: {err}") from err
|
||||
raise FitbitApiException(f"Server error response: {err}") from err
|
||||
except aiohttp.ClientError as err:
|
||||
raise FitbitApiException from err
|
||||
raise FitbitApiException(f"Client connection error: {err}") from err
|
||||
return cast(dict, await resp.json())
|
||||
|
||||
@property
|
||||
|
@@ -53,6 +53,21 @@ class OAuth2FlowHandler(
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_creation(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Create config entry from external data with Fitbit specific error handling."""
|
||||
try:
|
||||
return await super().async_step_creation()
|
||||
except FitbitAuthException as err:
|
||||
_LOGGER.error(
|
||||
"Failed to authenticate when creating Fitbit credentials: %s", err
|
||||
)
|
||||
return self.async_abort(reason="invalid_auth")
|
||||
except FitbitApiException as err:
|
||||
_LOGGER.error("Failed to create Fitbit credentials: %s", err)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
|
||||
"""Create an entry for the flow, or update existing entry."""
|
||||
|
||||
|
@@ -8,6 +8,8 @@ import logging
|
||||
import os
|
||||
from typing import Any, Final, cast
|
||||
|
||||
from fitbit import Fitbit
|
||||
from oauthlib.oauth2.rfc6749.errors import OAuth2Error
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.application_credentials import (
|
||||
@@ -567,34 +569,54 @@ async def async_setup_platform(
|
||||
|
||||
if config_file is not None:
|
||||
_LOGGER.debug("Importing existing fitbit.conf application credentials")
|
||||
await async_import_client_credential(
|
||||
hass,
|
||||
DOMAIN,
|
||||
ClientCredential(
|
||||
config_file[CONF_CLIENT_ID], config_file[CONF_CLIENT_SECRET]
|
||||
),
|
||||
|
||||
# Refresh the token before importing to ensure it is working and not
|
||||
# expired on first initialization.
|
||||
authd_client = Fitbit(
|
||||
config_file[CONF_CLIENT_ID],
|
||||
config_file[CONF_CLIENT_SECRET],
|
||||
access_token=config_file[ATTR_ACCESS_TOKEN],
|
||||
refresh_token=config_file[ATTR_REFRESH_TOKEN],
|
||||
expires_at=config_file[ATTR_LAST_SAVED_AT],
|
||||
refresh_cb=lambda x: None,
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
"auth_implementation": DOMAIN,
|
||||
CONF_TOKEN: {
|
||||
ATTR_ACCESS_TOKEN: config_file[ATTR_ACCESS_TOKEN],
|
||||
ATTR_REFRESH_TOKEN: config_file[ATTR_REFRESH_TOKEN],
|
||||
"expires_at": config_file[ATTR_LAST_SAVED_AT],
|
||||
},
|
||||
CONF_CLOCK_FORMAT: config[CONF_CLOCK_FORMAT],
|
||||
CONF_UNIT_SYSTEM: config[CONF_UNIT_SYSTEM],
|
||||
CONF_MONITORED_RESOURCES: config[CONF_MONITORED_RESOURCES],
|
||||
},
|
||||
)
|
||||
translation_key = "deprecated_yaml_import"
|
||||
if (
|
||||
result.get("type") == FlowResultType.ABORT
|
||||
and result.get("reason") == "cannot_connect"
|
||||
):
|
||||
try:
|
||||
updated_token = await hass.async_add_executor_job(
|
||||
authd_client.client.refresh_token
|
||||
)
|
||||
except OAuth2Error as err:
|
||||
_LOGGER.debug("Unable to import fitbit OAuth2 credentials: %s", err)
|
||||
translation_key = "deprecated_yaml_import_issue_cannot_connect"
|
||||
else:
|
||||
await async_import_client_credential(
|
||||
hass,
|
||||
DOMAIN,
|
||||
ClientCredential(
|
||||
config_file[CONF_CLIENT_ID], config_file[CONF_CLIENT_SECRET]
|
||||
),
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
"auth_implementation": DOMAIN,
|
||||
CONF_TOKEN: {
|
||||
ATTR_ACCESS_TOKEN: updated_token[ATTR_ACCESS_TOKEN],
|
||||
ATTR_REFRESH_TOKEN: updated_token[ATTR_REFRESH_TOKEN],
|
||||
"expires_at": updated_token["expires_at"],
|
||||
"scope": " ".join(updated_token.get("scope", [])),
|
||||
},
|
||||
CONF_CLOCK_FORMAT: config[CONF_CLOCK_FORMAT],
|
||||
CONF_UNIT_SYSTEM: config[CONF_UNIT_SYSTEM],
|
||||
CONF_MONITORED_RESOURCES: config[CONF_MONITORED_RESOURCES],
|
||||
},
|
||||
)
|
||||
translation_key = "deprecated_yaml_import"
|
||||
if (
|
||||
result.get("type") == FlowResultType.ABORT
|
||||
and result.get("reason") == "cannot_connect"
|
||||
):
|
||||
translation_key = "deprecated_yaml_import_issue_cannot_connect"
|
||||
else:
|
||||
translation_key = "deprecated_yaml_no_import"
|
||||
|
||||
|
@@ -16,9 +16,10 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"wrong_account": "The user credentials provided do not match this Fitbit account."
|
||||
|
@@ -139,9 +139,9 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
return self._device_information["fwVersion"]
|
||||
|
||||
@property
|
||||
def serial_number(self) -> str:
|
||||
def serial_number(self) -> str | None:
|
||||
"""Return the serial number for the device."""
|
||||
return self._device_information["serialNumber"]
|
||||
return self._device_information.get("serialNumber")
|
||||
|
||||
@property
|
||||
def pending_info_alerts_count(self) -> int:
|
||||
|
@@ -16,7 +16,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from .const import (
|
||||
@@ -204,7 +204,7 @@ class FroniusSolarNet:
|
||||
|
||||
# Only for re-scans. Initial setup adds entities through sensor.async_setup_entry
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
|
||||
async_dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
|
||||
|
||||
_LOGGER.debug(
|
||||
"New inverter added (UID: %s)",
|
||||
|
@@ -661,7 +661,7 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
|
||||
if new_value is None:
|
||||
return self.entity_description.default_value
|
||||
if self.entity_description.invalid_when_falsy and not new_value:
|
||||
raise ValueError(f"Ignoring zero value for {self.entity_id}.")
|
||||
return None
|
||||
if isinstance(new_value, float):
|
||||
return round(new_value, 4)
|
||||
return new_value
|
||||
@@ -671,10 +671,9 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
|
||||
"""Handle updated data from the coordinator."""
|
||||
try:
|
||||
self._attr_native_value = self._get_entity_value()
|
||||
except (KeyError, ValueError):
|
||||
except KeyError:
|
||||
# sets state to `None` if no default_value is defined in entity description
|
||||
# KeyError: raised when omitted in response - eg. at night when no production
|
||||
# ValueError: raised when invalid zero value received
|
||||
self._attr_native_value = self.entity_description.default_value
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
@@ -388,6 +388,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
# Can be removed in 2023
|
||||
hass.http.register_redirect("/config/server_control", "/developer-tools/yaml")
|
||||
|
||||
# Shopping list panel was replaced by todo panel in 2023.11
|
||||
hass.http.register_redirect("/shopping-list", "/todo")
|
||||
|
||||
hass.http.app.router.register_resource(IndexView(repo_path, hass))
|
||||
|
||||
async_register_built_in_panel(hass, "profile")
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20231025.1"]
|
||||
"requirements": ["home-assistant-frontend==20231030.2"]
|
||||
}
|
||||
|
@@ -88,7 +88,6 @@ DESCRIPTIONS = (
|
||||
GardenaBluetoothSensorEntityDescription(
|
||||
key=Sensor.measurement_timestamp.uuid,
|
||||
translation_key="sensor_measurement_timestamp",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
char=Sensor.measurement_timestamp,
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/geniushub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["geniushubclient"],
|
||||
"requirements": ["geniushub-client==0.7.0"]
|
||||
"requirements": ["geniushub-client==0.7.1"]
|
||||
}
|
||||
|
@@ -1,12 +1,9 @@
|
||||
"""Support for Google Mail."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp.client_exceptions import ClientError, ClientResponseError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
@@ -35,16 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
auth = AsyncConfigEntryAuth(session)
|
||||
try:
|
||||
await auth.check_and_refresh_token()
|
||||
except ClientResponseError as err:
|
||||
if 400 <= err.status < 500:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"OAuth session is not valid, reauth required"
|
||||
) from err
|
||||
raise ConfigEntryNotReady from err
|
||||
except ClientError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
await auth.check_and_refresh_token()
|
||||
hass.data[DOMAIN][entry.entry_id] = auth
|
||||
|
||||
hass.async_create_task(
|
||||
|
@@ -1,9 +1,16 @@
|
||||
"""API for Google Mail bound to Home Assistant OAuth."""
|
||||
from aiohttp.client_exceptions import ClientError, ClientResponseError
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import Resource, build
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
|
||||
@@ -24,14 +31,30 @@ class AsyncConfigEntryAuth:
|
||||
|
||||
async def check_and_refresh_token(self) -> str:
|
||||
"""Check the token."""
|
||||
await self.oauth_session.async_ensure_token_valid()
|
||||
try:
|
||||
await self.oauth_session.async_ensure_token_valid()
|
||||
except (RefreshError, ClientResponseError, ClientError) as ex:
|
||||
if (
|
||||
self.oauth_session.config_entry.state
|
||||
is ConfigEntryState.SETUP_IN_PROGRESS
|
||||
):
|
||||
if isinstance(ex, ClientResponseError) and 400 <= ex.status < 500:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"OAuth session is not valid, reauth required"
|
||||
) from ex
|
||||
raise ConfigEntryNotReady from ex
|
||||
if (
|
||||
isinstance(ex, RefreshError)
|
||||
or hasattr(ex, "status")
|
||||
and ex.status == 400
|
||||
):
|
||||
self.oauth_session.config_entry.async_start_reauth(
|
||||
self.oauth_session.hass
|
||||
)
|
||||
raise HomeAssistantError(ex) from ex
|
||||
return self.access_token
|
||||
|
||||
async def get_resource(self) -> Resource:
|
||||
"""Get current resource."""
|
||||
try:
|
||||
credentials = Credentials(await self.check_and_refresh_token())
|
||||
except RefreshError as ex:
|
||||
self.oauth_session.config_entry.async_start_reauth(self.oauth_session.hass)
|
||||
raise ex
|
||||
credentials = Credentials(await self.check_and_refresh_token())
|
||||
return build("gmail", "v1", credentials=credentials)
|
||||
|
@@ -2,6 +2,13 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
from googleapiclient.http import HttpRequest
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .const import DOMAIN, OAUTH2_SCOPES
|
||||
@@ -28,3 +35,24 @@ class OAuth2FlowHandler(
|
||||
"access_type": "offline",
|
||||
"prompt": "consent",
|
||||
}
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
|
||||
"""Create an entry for the flow."""
|
||||
try:
|
||||
resource = build(
|
||||
"tasks",
|
||||
"v1",
|
||||
credentials=Credentials(token=data[CONF_TOKEN][CONF_ACCESS_TOKEN]),
|
||||
)
|
||||
cmd: HttpRequest = resource.tasklists().list()
|
||||
await self.hass.async_add_executor_job(cmd.execute)
|
||||
except HttpError as ex:
|
||||
error = ex.reason
|
||||
return self.async_abort(
|
||||
reason="access_not_configured",
|
||||
description_placeholders={"message": error},
|
||||
)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
self.logger.exception("Unknown error occurred: %s", ex)
|
||||
return self.async_abort(reason="unknown")
|
||||
return self.async_create_entry(title=self.flow_impl.name, data=data)
|
||||
|
@@ -15,7 +15,9 @@
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
||||
"access_not_configured": "Unable to access the Google API:\n\n{message}",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhap"],
|
||||
"requirements": [
|
||||
"HAP-python==4.9.0",
|
||||
"HAP-python==4.9.1",
|
||||
"fnv-hash-fast==0.5.0",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1"
|
||||
|
@@ -884,7 +884,9 @@ class HKDevice:
|
||||
self._config_changed_callbacks.add(callback_)
|
||||
return partial(self._remove_config_changed_callback, callback_)
|
||||
|
||||
async def get_characteristics(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
||||
async def get_characteristics(
|
||||
self, *args: Any, **kwargs: Any
|
||||
) -> dict[tuple[int, int], dict[str, Any]]:
|
||||
"""Read latest state from homekit accessory."""
|
||||
return await self.pairing.get_characteristics(*args, **kwargs)
|
||||
|
||||
|
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.0.8"],
|
||||
"requirements": ["aiohomekit==3.0.9"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ async def async_setup_entry(
|
||||
class HomeWizardIdentifyButton(HomeWizardEntity, ButtonEntity):
|
||||
"""Representation of a identify button."""
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_device_class = ButtonDeviceClass.IDENTIFY
|
||||
|
||||
def __init__(
|
||||
|
@@ -62,7 +62,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
self._abort_if_unique_id_configured(updates=user_input)
|
||||
return self.async_create_entry(
|
||||
title=f"{device_info.product_name} ({device_info.serial})",
|
||||
title=f"{device_info.product_name}",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
@@ -121,14 +121,18 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors = {"base": ex.error_code}
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=f"{self.discovery.product_name} ({self.discovery.serial})",
|
||||
title=self.discovery.product_name,
|
||||
data={CONF_IP_ADDRESS: self.discovery.ip},
|
||||
)
|
||||
|
||||
self._set_confirm_only()
|
||||
self.context["title_placeholders"] = {
|
||||
"name": f"{self.discovery.product_name} ({self.discovery.serial})"
|
||||
}
|
||||
|
||||
# We won't be adding mac/serial to the title for devices
|
||||
# that users generally don't have multiple of.
|
||||
name = self.discovery.product_name
|
||||
if self.discovery.product_type not in ["HWE-P1", "HWE-WTR"]:
|
||||
name = f"{name} ({self.discovery.serial})"
|
||||
self.context["title_placeholders"] = {"name": name}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
|
@@ -28,18 +28,23 @@ async def async_get_config_entry_diagnostics(
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
meter_data = {
|
||||
"device": asdict(coordinator.data.device),
|
||||
"data": asdict(coordinator.data.data),
|
||||
"state": asdict(coordinator.data.state)
|
||||
if coordinator.data.state is not None
|
||||
else None,
|
||||
"system": asdict(coordinator.data.system)
|
||||
if coordinator.data.system is not None
|
||||
else None,
|
||||
}
|
||||
state: dict[str, Any] | None = None
|
||||
if coordinator.data.state:
|
||||
state = asdict(coordinator.data.state)
|
||||
|
||||
return {
|
||||
"entry": async_redact_data(entry.data, TO_REDACT),
|
||||
"data": async_redact_data(meter_data, TO_REDACT),
|
||||
}
|
||||
system: dict[str, Any] | None = None
|
||||
if coordinator.data.system:
|
||||
system = asdict(coordinator.data.system)
|
||||
|
||||
return async_redact_data(
|
||||
{
|
||||
"entry": async_redact_data(entry.data, TO_REDACT),
|
||||
"data": {
|
||||
"device": asdict(coordinator.data.device),
|
||||
"data": asdict(coordinator.data.data),
|
||||
"state": state,
|
||||
"system": system,
|
||||
},
|
||||
},
|
||||
TO_REDACT,
|
||||
)
|
||||
|
@@ -18,17 +18,13 @@ class HomeWizardEntity(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator]):
|
||||
"""Initialize the HomeWizard entity."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=coordinator.entry.title,
|
||||
manufacturer="HomeWizard",
|
||||
sw_version=coordinator.data.device.firmware_version,
|
||||
model=coordinator.data.device.product_type,
|
||||
)
|
||||
|
||||
if coordinator.data.device.serial is not None:
|
||||
if (serial_number := coordinator.data.device.serial) is not None:
|
||||
self._attr_device_info[ATTR_CONNECTIONS] = {
|
||||
(CONNECTION_NETWORK_MAC, coordinator.data.device.serial)
|
||||
}
|
||||
|
||||
self._attr_device_info[ATTR_IDENTIFIERS] = {
|
||||
(DOMAIN, coordinator.data.device.serial)
|
||||
(CONNECTION_NETWORK_MAC, serial_number)
|
||||
}
|
||||
self._attr_device_info[ATTR_IDENTIFIERS] = {(DOMAIN, serial_number)}
|
||||
|
@@ -47,13 +47,17 @@ class HWEnergyNumberEntity(HomeWizardEntity, NumberEntity):
|
||||
await self.coordinator.api.state_set(brightness=int(value * (255 / 100)))
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.coordinator.data.state is not None
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the current value."""
|
||||
if (
|
||||
self.coordinator.data.state is None
|
||||
or self.coordinator.data.state.brightness is None
|
||||
not self.coordinator.data.state
|
||||
or (brightness := self.coordinator.data.state.brightness) is None
|
||||
):
|
||||
return None
|
||||
brightness: float = self.coordinator.data.state.brightness
|
||||
return round(brightness * (100 / 255))
|
||||
|
@@ -38,6 +38,7 @@ from .const import (
|
||||
CONF_COOL_AWAY_TEMPERATURE,
|
||||
CONF_HEAT_AWAY_TEMPERATURE,
|
||||
DOMAIN,
|
||||
RETRY,
|
||||
)
|
||||
|
||||
ATTR_FAN_ACTION = "fan_action"
|
||||
@@ -155,6 +156,7 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
self._cool_away_temp = cool_away_temp
|
||||
self._heat_away_temp = heat_away_temp
|
||||
self._away = False
|
||||
self._retry = 0
|
||||
|
||||
self._attr_unique_id = device.deviceid
|
||||
|
||||
@@ -351,6 +353,11 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
if mode == "heat":
|
||||
await self._device.set_setpoint_heat(temperature)
|
||||
|
||||
except UnexpectedResponse as err:
|
||||
raise HomeAssistantError(
|
||||
"Honeywell set temperature failed: Invalid Response"
|
||||
) from err
|
||||
|
||||
except SomeComfortError as err:
|
||||
_LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
|
||||
raise ValueError(
|
||||
@@ -367,6 +374,11 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW):
|
||||
await self._device.set_setpoint_heat(temperature)
|
||||
|
||||
except UnexpectedResponse as err:
|
||||
raise HomeAssistantError(
|
||||
"Honeywell set temperature failed: Invalid Response"
|
||||
) from err
|
||||
|
||||
except SomeComfortError as err:
|
||||
_LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
|
||||
raise ValueError(
|
||||
@@ -483,21 +495,28 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
try:
|
||||
await self._device.refresh()
|
||||
self._attr_available = True
|
||||
self._retry = 0
|
||||
|
||||
except UnauthorizedError:
|
||||
try:
|
||||
await self._data.client.login()
|
||||
await self._device.refresh()
|
||||
self._attr_available = True
|
||||
self._retry = 0
|
||||
|
||||
except (
|
||||
SomeComfortError,
|
||||
ClientConnectionError,
|
||||
asyncio.TimeoutError,
|
||||
):
|
||||
self._attr_available = False
|
||||
self._retry += 1
|
||||
if self._retry > RETRY:
|
||||
self._attr_available = False
|
||||
|
||||
except (ClientConnectionError, asyncio.TimeoutError):
|
||||
self._attr_available = False
|
||||
self._retry += 1
|
||||
if self._retry > RETRY:
|
||||
self._attr_available = False
|
||||
|
||||
except UnexpectedResponse:
|
||||
pass
|
||||
|
@@ -10,3 +10,4 @@ DEFAULT_HEAT_AWAY_TEMPERATURE = 61
|
||||
CONF_DEV_ID = "thermostat"
|
||||
CONF_LOC_ID = "location"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
RETRY = 3
|
||||
|
@@ -146,7 +146,7 @@ SENSORS_INFO = [
|
||||
translation_key="energy_today",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
key=SOURCE_TYPE_ELECTRICITY,
|
||||
sensor_type=SENSOR_TYPE_THIS_DAY,
|
||||
precision=1,
|
||||
@@ -156,7 +156,7 @@ SENSORS_INFO = [
|
||||
translation_key="energy_week",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
key=SOURCE_TYPE_ELECTRICITY,
|
||||
sensor_type=SENSOR_TYPE_THIS_WEEK,
|
||||
precision=1,
|
||||
@@ -166,7 +166,7 @@ SENSORS_INFO = [
|
||||
translation_key="energy_month",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
key=SOURCE_TYPE_ELECTRICITY,
|
||||
sensor_type=SENSOR_TYPE_THIS_MONTH,
|
||||
precision=1,
|
||||
@@ -176,7 +176,7 @@ SENSORS_INFO = [
|
||||
translation_key="energy_year",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
key=SOURCE_TYPE_ELECTRICITY,
|
||||
sensor_type=SENSOR_TYPE_THIS_YEAR,
|
||||
precision=1,
|
||||
@@ -197,7 +197,7 @@ SENSORS_INFO = [
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
key=SOURCE_TYPE_GAS,
|
||||
sensor_type=SENSOR_TYPE_THIS_DAY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
icon="mdi:counter",
|
||||
precision=1,
|
||||
),
|
||||
@@ -207,7 +207,7 @@ SENSORS_INFO = [
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
key=SOURCE_TYPE_GAS,
|
||||
sensor_type=SENSOR_TYPE_THIS_WEEK,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
icon="mdi:counter",
|
||||
precision=1,
|
||||
),
|
||||
@@ -217,7 +217,7 @@ SENSORS_INFO = [
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
key=SOURCE_TYPE_GAS,
|
||||
sensor_type=SENSOR_TYPE_THIS_MONTH,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
icon="mdi:counter",
|
||||
precision=1,
|
||||
),
|
||||
@@ -227,7 +227,7 @@ SENSORS_INFO = [
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
key=SOURCE_TYPE_GAS,
|
||||
sensor_type=SENSOR_TYPE_THIS_YEAR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
icon="mdi:counter",
|
||||
precision=1,
|
||||
),
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
from pydrawise import legacy
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
@@ -13,11 +12,10 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
from .const import DOMAIN, SCAN_INTERVAL
|
||||
from .coordinator import HydrawiseDataUpdateCoordinator
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@@ -53,24 +51,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up Hydrawise from a config entry."""
|
||||
access_token = config_entry.data[CONF_API_KEY]
|
||||
try:
|
||||
hydrawise = await hass.async_add_executor_job(
|
||||
legacy.LegacyHydrawise, access_token
|
||||
)
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
LOGGER.error("Unable to connect to Hydrawise cloud service: %s", str(ex))
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unable to connect to Hydrawise cloud service: {ex}"
|
||||
) from ex
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[
|
||||
config_entry.entry_id
|
||||
] = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL)
|
||||
if not hydrawise.controller_info or not hydrawise.controller_status:
|
||||
raise ConfigEntryNotReady("Hydrawise data not loaded")
|
||||
|
||||
# NOTE: We don't need to call async_config_entry_first_refresh() because
|
||||
# data is fetched when the Hydrawiser object is instantiated.
|
||||
hydrawise = legacy.LegacyHydrawise(access_token, load_on_init=False)
|
||||
coordinator = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
@@ -12,12 +12,12 @@ from homeassistant.components.binary_sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HydrawiseDataUpdateCoordinator
|
||||
from .entity import HydrawiseEntity
|
||||
|
||||
@@ -95,13 +95,10 @@ async def async_setup_entry(
|
||||
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
|
||||
"""A sensor implementation for Hydrawise device."""
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Get the latest data and updates the state."""
|
||||
LOGGER.debug("Updating Hydrawise binary sensor: %s", self.name)
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
if self.entity_description.key == "status":
|
||||
self._attr_is_on = self.coordinator.last_update_success
|
||||
elif self.entity_description.key == "is_watering":
|
||||
relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]]
|
||||
self._attr_is_on = relay_data["timestr"] == "Now"
|
||||
super()._handle_coordinator_update()
|
||||
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -36,3 +37,14 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]):
|
||||
name=data["name"],
|
||||
manufacturer=MANUFACTURER,
|
||||
)
|
||||
self._update_attrs()
|
||||
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
return # pragma: no cover
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Get the latest data and updates the state."""
|
||||
self._update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
@@ -11,13 +11,13 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HydrawiseDataUpdateCoordinator
|
||||
from .entity import HydrawiseEntity
|
||||
|
||||
@@ -82,10 +82,8 @@ async def async_setup_entry(
|
||||
class HydrawiseSensor(HydrawiseEntity, SensorEntity):
|
||||
"""A sensor implementation for Hydrawise device."""
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Get the latest data and updates the states."""
|
||||
LOGGER.debug("Updating Hydrawise sensor: %s", self.name)
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]]
|
||||
if self.entity_description.key == "watering_time":
|
||||
if relay_data["timestr"] == "Now":
|
||||
@@ -94,8 +92,6 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity):
|
||||
self._attr_native_value = 0
|
||||
else: # _sensor_type == 'next_cycle'
|
||||
next_cycle = min(relay_data["time"], TWO_YEAR_SECONDS)
|
||||
LOGGER.debug("New cycle time: %s", next_cycle)
|
||||
self._attr_native_value = dt_util.utc_from_timestamp(
|
||||
dt_util.as_timestamp(dt_util.now()) + next_cycle
|
||||
)
|
||||
super()._handle_coordinator_update()
|
||||
|
@@ -13,7 +13,7 @@ from homeassistant.components.switch import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -23,7 +23,6 @@ from .const import (
|
||||
CONF_WATERING_TIME,
|
||||
DEFAULT_WATERING_TIME,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
from .coordinator import HydrawiseDataUpdateCoordinator
|
||||
from .entity import HydrawiseEntity
|
||||
@@ -124,14 +123,11 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity):
|
||||
elif self.entity_description.key == "auto_watering":
|
||||
self.coordinator.api.suspend_zone(365, zone_number)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Update device state."""
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
zone_number = self.data["relay"]
|
||||
LOGGER.debug("Updating Hydrawise switch: %s", self.name)
|
||||
timestr = self.coordinator.api.relays_by_zone_number[zone_number]["timestr"]
|
||||
if self.entity_description.key == "manual_watering":
|
||||
self._attr_is_on = timestr == "Now"
|
||||
elif self.entity_description.key == "auto_watering":
|
||||
self._attr_is_on = timestr not in {"", "Now"}
|
||||
super()._handle_coordinator_update()
|
||||
|
@@ -78,7 +78,7 @@ class IslamicPrayerTimeSensor(
|
||||
"""Initialize the Islamic prayer time sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = description.key
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
||||
name=NAME,
|
||||
|
@@ -122,12 +122,12 @@ class KNXExposeSensor:
|
||||
"""Extract value from state."""
|
||||
if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
|
||||
value = self.expose_default
|
||||
elif self.expose_attribute is not None:
|
||||
_attr = state.attributes.get(self.expose_attribute)
|
||||
value = _attr if _attr is not None else self.expose_default
|
||||
else:
|
||||
value = (
|
||||
state.state
|
||||
if self.expose_attribute is None
|
||||
else state.attributes.get(self.expose_attribute, self.expose_default)
|
||||
)
|
||||
value = state.state
|
||||
|
||||
if self.expose_type == "binary":
|
||||
if value in (1, STATE_ON, "True"):
|
||||
return True
|
||||
|
@@ -12,7 +12,7 @@
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"xknx==2.11.2",
|
||||
"xknxproject==3.3.0",
|
||||
"xknxproject==3.4.0",
|
||||
"knx-frontend==2023.6.23.191712"
|
||||
]
|
||||
}
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluetooth-data-tools==1.13.0", "ld2410-ble==0.1.1"]
|
||||
"requirements": ["bluetooth-data-tools==1.14.0", "ld2410-ble==0.1.1"]
|
||||
}
|
||||
|
@@ -32,5 +32,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/led_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["bluetooth-data-tools==1.13.0", "led-ble==1.0.1"]
|
||||
"requirements": ["bluetooth-data-tools==1.14.0", "led-ble==1.0.1"]
|
||||
}
|
||||
|
@@ -149,31 +149,29 @@ async def _async_reproduce_state(
|
||||
service = SERVICE_TURN_ON
|
||||
for attr in ATTR_GROUP:
|
||||
# All attributes that are not colors
|
||||
if attr in state.attributes:
|
||||
service_data[attr] = state.attributes[attr]
|
||||
if (attr_state := state.attributes.get(attr)) is not None:
|
||||
service_data[attr] = attr_state
|
||||
|
||||
if (
|
||||
state.attributes.get(ATTR_COLOR_MODE, ColorMode.UNKNOWN)
|
||||
!= ColorMode.UNKNOWN
|
||||
):
|
||||
color_mode = state.attributes[ATTR_COLOR_MODE]
|
||||
if color_mode_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
|
||||
if color_mode_attr.state_attr not in state.attributes:
|
||||
if cm_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
|
||||
if (cm_attr_state := state.attributes.get(cm_attr.state_attr)) is None:
|
||||
_LOGGER.warning(
|
||||
"Color mode %s specified but attribute %s missing for: %s",
|
||||
color_mode,
|
||||
color_mode_attr.state_attr,
|
||||
cm_attr.state_attr,
|
||||
state.entity_id,
|
||||
)
|
||||
return
|
||||
service_data[color_mode_attr.parameter] = state.attributes[
|
||||
color_mode_attr.state_attr
|
||||
]
|
||||
service_data[cm_attr.parameter] = cm_attr_state
|
||||
else:
|
||||
# Fall back to Choosing the first color that is specified
|
||||
for color_attr in COLOR_GROUP:
|
||||
if color_attr in state.attributes:
|
||||
service_data[color_attr] = state.attributes[color_attr]
|
||||
if (color_attr_state := state.attributes.get(color_attr)) is not None:
|
||||
service_data[color_attr] = color_attr_state
|
||||
break
|
||||
|
||||
elif state.state == STATE_OFF:
|
||||
|
@@ -139,20 +139,28 @@ class LocalTodoListEntity(TodoListEntity):
|
||||
await self._async_save()
|
||||
await self.async_update_ha_state(force_refresh=True)
|
||||
|
||||
async def async_move_todo_item(self, uid: str, pos: int) -> None:
|
||||
async def async_move_todo_item(
|
||||
self, uid: str, previous_uid: str | None = None
|
||||
) -> None:
|
||||
"""Re-order an item to the To-do list."""
|
||||
if uid == previous_uid:
|
||||
return
|
||||
todos = self._calendar.todos
|
||||
found_item: Todo | None = None
|
||||
for idx, itm in enumerate(todos):
|
||||
if itm.uid == uid:
|
||||
found_item = itm
|
||||
todos.pop(idx)
|
||||
break
|
||||
if found_item is None:
|
||||
item_idx: dict[str, int] = {itm.uid: idx for idx, itm in enumerate(todos)}
|
||||
if uid not in item_idx:
|
||||
raise HomeAssistantError(
|
||||
f"Item '{uid}' not found in todo list {self.entity_id}"
|
||||
"Item '{uid}' not found in todo list {self.entity_id}"
|
||||
)
|
||||
todos.insert(pos, found_item)
|
||||
if previous_uid and previous_uid not in item_idx:
|
||||
raise HomeAssistantError(
|
||||
"Item '{previous_uid}' not found in todo list {self.entity_id}"
|
||||
)
|
||||
dst_idx = item_idx[previous_uid] + 1 if previous_uid else 0
|
||||
src_idx = item_idx[uid]
|
||||
src_item = todos.pop(src_idx)
|
||||
if dst_idx > src_idx:
|
||||
dst_idx -= 1
|
||||
todos.insert(dst_idx, src_item)
|
||||
await self._async_save()
|
||||
await self.async_update_ha_state(force_refresh=True)
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_NAME, CONF_WEBHOOK_ID
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_NAME, CONF_WEBHOOK_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -95,7 +95,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# Check if already exists
|
||||
await self.async_set_unique_id(lock_data["bridge_mac_wifi"])
|
||||
self._abort_if_unique_id_configured({CONF_HOST: host})
|
||||
self._abort_if_unique_id_configured({"bridge_ip": host})
|
||||
|
||||
return await self.async_step_user()
|
||||
|
||||
|
@@ -60,6 +60,10 @@ async def websocket_lovelace_resources(
|
||||
"""Send Lovelace UI resources over WebSocket configuration."""
|
||||
resources = hass.data[DOMAIN]["resources"]
|
||||
|
||||
if hass.config.safe_mode:
|
||||
connection.send_result(msg["id"], [])
|
||||
return
|
||||
|
||||
if not resources.loaded:
|
||||
await resources.async_load()
|
||||
resources.loaded = True
|
||||
|
@@ -28,9 +28,10 @@ def setup_platform(
|
||||
|
||||
data = hass.data[LUPUSEC_DOMAIN]
|
||||
|
||||
devices = []
|
||||
device_types = [CONST.TYPE_SWITCH]
|
||||
|
||||
for device in data.lupusec.get_devices(generic_type=CONST.TYPE_SWITCH):
|
||||
devices = []
|
||||
for device in data.lupusec.get_devices(generic_type=device_types):
|
||||
devices.append(LupusecSwitch(data, device))
|
||||
|
||||
add_entities(devices)
|
||||
|
@@ -1,12 +1,15 @@
|
||||
"""Matter lock."""
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import IntFlag
|
||||
from typing import Any
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
|
||||
from homeassistant.components.lock import LockEntity, LockEntityDescription
|
||||
from homeassistant.components.lock import (
|
||||
LockEntity,
|
||||
LockEntityDescription,
|
||||
LockEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_CODE, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -17,6 +20,8 @@ from .entity import MatterEntity
|
||||
from .helpers import get_matter
|
||||
from .models import MatterDiscoverySchema
|
||||
|
||||
DoorLockFeature = clusters.DoorLock.Bitmaps.Feature
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -61,6 +66,14 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
|
||||
return bool(self.features & DoorLockFeature.kDoorPositionSensor)
|
||||
|
||||
@property
|
||||
def supports_unbolt(self) -> bool:
|
||||
"""Return True if the lock supports unbolt."""
|
||||
if self.features is None:
|
||||
return False
|
||||
|
||||
return bool(self.features & DoorLockFeature.kUnbolt)
|
||||
|
||||
async def send_device_command(
|
||||
self,
|
||||
command: clusters.ClusterCommand,
|
||||
@@ -92,6 +105,25 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
self._lock_option_default_code,
|
||||
)
|
||||
code_bytes = code.encode() if code else None
|
||||
if self.supports_unbolt:
|
||||
# if the lock reports it has separate unbolt support,
|
||||
# the unlock command should unbolt only on the unlock command
|
||||
# and unlatch on the HA 'open' command.
|
||||
await self.send_device_command(
|
||||
command=clusters.DoorLock.Commands.UnboltDoor(code_bytes)
|
||||
)
|
||||
else:
|
||||
await self.send_device_command(
|
||||
command=clusters.DoorLock.Commands.UnlockDoor(code_bytes)
|
||||
)
|
||||
|
||||
async def async_open(self, **kwargs: Any) -> None:
|
||||
"""Open the door latch."""
|
||||
code: str = kwargs.get(
|
||||
ATTR_CODE,
|
||||
self._lock_option_default_code,
|
||||
)
|
||||
code_bytes = code.encode() if code else None
|
||||
await self.send_device_command(
|
||||
command=clusters.DoorLock.Commands.UnlockDoor(code_bytes)
|
||||
)
|
||||
@@ -104,6 +136,8 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
self.features = int(
|
||||
self.get_matter_attribute_value(clusters.DoorLock.Attributes.FeatureMap)
|
||||
)
|
||||
if self.supports_unbolt:
|
||||
self._attr_supported_features = LockEntityFeature.OPEN
|
||||
|
||||
lock_state = self.get_matter_attribute_value(
|
||||
clusters.DoorLock.Attributes.LockState
|
||||
@@ -144,26 +178,6 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
)
|
||||
|
||||
|
||||
class DoorLockFeature(IntFlag):
|
||||
"""Temp enum that represents the features of a door lock.
|
||||
|
||||
Should be replaced by the library provided one once that is released.
|
||||
"""
|
||||
|
||||
kPinCredential = 0x1 # noqa: N815
|
||||
kRfidCredential = 0x2 # noqa: N815
|
||||
kFingerCredentials = 0x4 # noqa: N815
|
||||
kLogging = 0x8 # noqa: N815
|
||||
kWeekDayAccessSchedules = 0x10 # noqa: N815
|
||||
kDoorPositionSensor = 0x20 # noqa: N815
|
||||
kFaceCredentials = 0x40 # noqa: N815
|
||||
kCredentialsOverTheAirAccess = 0x80 # noqa: N815
|
||||
kUser = 0x100 # noqa: N815
|
||||
kNotification = 0x200 # noqa: N815
|
||||
kYearDayAccessSchedules = 0x400 # noqa: N815
|
||||
kHolidaySchedules = 0x800 # noqa: N815
|
||||
|
||||
|
||||
DISCOVERY_SCHEMAS = [
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.LOCK,
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp==2023.9.24"]
|
||||
"requirements": ["yt-dlp==2023.10.13"]
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -68,6 +69,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
await cleanup_old_device(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -88,6 +91,15 @@ async def async_update_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def cleanup_old_device(hass: HomeAssistant) -> None:
|
||||
"""Cleanup device without proper device identifier."""
|
||||
device_reg = dr.async_get(hass)
|
||||
device = device_reg.async_get_device(identifiers={(DOMAIN,)}) # type: ignore[arg-type]
|
||||
if device:
|
||||
_LOGGER.debug("Removing improper device %s", device.name)
|
||||
device_reg.async_remove_device(device.id)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Unable to connect to the web site."""
|
||||
|
||||
|
@@ -31,13 +31,21 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import entity_registry as er, sun
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from . import MetDataUpdateCoordinator
|
||||
from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP
|
||||
from .const import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_MAP,
|
||||
CONDITIONS_MAP,
|
||||
CONF_TRACK_HOME,
|
||||
DOMAIN,
|
||||
FORECAST_MAP,
|
||||
)
|
||||
|
||||
DEFAULT_NAME = "Met.no"
|
||||
|
||||
@@ -60,7 +68,7 @@ async def async_setup_entry(
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(name, str)
|
||||
|
||||
entities = [MetWeather(coordinator, config_entry.data, False, name, is_metric)]
|
||||
entities = [MetWeather(coordinator, config_entry, False, name, is_metric)]
|
||||
|
||||
# Add hourly entity to legacy config entries
|
||||
if entity_registry.async_get_entity_id(
|
||||
@@ -69,9 +77,7 @@ async def async_setup_entry(
|
||||
_calculate_unique_id(config_entry.data, True),
|
||||
):
|
||||
name = f"{name} hourly"
|
||||
entities.append(
|
||||
MetWeather(coordinator, config_entry.data, True, name, is_metric)
|
||||
)
|
||||
entities.append(MetWeather(coordinator, config_entry, True, name, is_metric))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -114,22 +120,22 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MetDataUpdateCoordinator,
|
||||
config: MappingProxyType[str, Any],
|
||||
config_entry: ConfigEntry,
|
||||
hourly: bool,
|
||||
name: str,
|
||||
is_metric: bool,
|
||||
) -> None:
|
||||
"""Initialise the platform with a data instance and site."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = _calculate_unique_id(config, hourly)
|
||||
self._config = config
|
||||
self._attr_unique_id = _calculate_unique_id(config_entry.data, hourly)
|
||||
self._config = config_entry.data
|
||||
self._is_metric = is_metric
|
||||
self._hourly = hourly
|
||||
self._attr_entity_registry_enabled_default = not hourly
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name="Forecast",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN,)}, # type: ignore[arg-type]
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Met.no",
|
||||
model="Forecast",
|
||||
configuration_url="https://www.met.no/en",
|
||||
@@ -143,6 +149,10 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]):
|
||||
condition = self.coordinator.data.current_weather_data.get("condition")
|
||||
if condition is None:
|
||||
return None
|
||||
|
||||
if condition == ATTR_CONDITION_SUNNY and not sun.is_up(self.hass):
|
||||
condition = ATTR_CONDITION_CLEAR_NIGHT
|
||||
|
||||
return format_condition(condition)
|
||||
|
||||
@property
|
||||
|
@@ -168,6 +168,7 @@ class SlaveSensor(
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{idx}"
|
||||
self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
self._attr_state_class = entry.get(CONF_STATE_CLASS)
|
||||
self._attr_device_class = entry.get(CONF_DEVICE_CLASS)
|
||||
self._attr_available = False
|
||||
super().__init__(coordinator)
|
||||
|
||||
|
@@ -63,8 +63,8 @@ PARM_IS_LEGAL = namedtuple(
|
||||
],
|
||||
)
|
||||
# PARM_IS_LEGAL defines if the keywords:
|
||||
# count: ..
|
||||
# structure: ..
|
||||
# count:
|
||||
# structure:
|
||||
# swap: byte
|
||||
# swap: word
|
||||
# swap: word_byte (identical to swap: word)
|
||||
@@ -84,7 +84,7 @@ DEFAULT_STRUCT_FORMAT = {
|
||||
DataType.INT64: ENTRY("q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
|
||||
DataType.UINT64: ENTRY("Q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
|
||||
DataType.FLOAT64: ENTRY("d", 4, PARM_IS_LEGAL(False, False, True, True, True)),
|
||||
DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, False, False)),
|
||||
DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, True, False)),
|
||||
DataType.CUSTOM: ENTRY("?", 0, PARM_IS_LEGAL(True, True, False, False, False)),
|
||||
}
|
||||
|
||||
|
@@ -47,6 +47,7 @@ from .client import ( # noqa: F401
|
||||
publish,
|
||||
subscribe,
|
||||
)
|
||||
from .config import MQTT_BASE_SCHEMA, MQTT_RO_SCHEMA, MQTT_RW_SCHEMA # noqa: F401
|
||||
from .config_integration import CONFIG_SCHEMA_BASE
|
||||
from .const import ( # noqa: F401
|
||||
ATTR_PAYLOAD,
|
||||
@@ -232,7 +233,7 @@ async def async_check_config_schema(
|
||||
) -> None:
|
||||
"""Validate manually configured MQTT items."""
|
||||
mqtt_data = get_mqtt_data(hass)
|
||||
mqtt_config: list[dict[str, list[ConfigType]]] = config_yaml[DOMAIN]
|
||||
mqtt_config: list[dict[str, list[ConfigType]]] = config_yaml.get(DOMAIN, {})
|
||||
for mqtt_config_item in mqtt_config:
|
||||
for domain, config_items in mqtt_config_item.items():
|
||||
schema = mqtt_data.reload_schema[domain]
|
||||
|
@@ -232,16 +232,16 @@ TOPIC_KEYS = (
|
||||
def valid_preset_mode_configuration(config: ConfigType) -> ConfigType:
|
||||
"""Validate that the preset mode reset payload is not one of the preset modes."""
|
||||
if PRESET_NONE in config[CONF_PRESET_MODES_LIST]:
|
||||
raise ValueError("preset_modes must not include preset mode 'none'")
|
||||
raise vol.Invalid("preset_modes must not include preset mode 'none'")
|
||||
return config
|
||||
|
||||
|
||||
def valid_humidity_range_configuration(config: ConfigType) -> ConfigType:
|
||||
"""Validate a target_humidity range configuration, throws otherwise."""
|
||||
if config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX]:
|
||||
raise ValueError("target_humidity_max must be > target_humidity_min")
|
||||
raise vol.Invalid("target_humidity_max must be > target_humidity_min")
|
||||
if config[CONF_HUMIDITY_MAX] > 100:
|
||||
raise ValueError("max_humidity must be <= 100")
|
||||
raise vol.Invalid("max_humidity must be <= 100")
|
||||
|
||||
return config
|
||||
|
||||
|
@@ -116,16 +116,16 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def valid_speed_range_configuration(config: ConfigType) -> ConfigType:
|
||||
"""Validate that the fan speed_range configuration is valid, throws if it isn't."""
|
||||
if config[CONF_SPEED_RANGE_MIN] == 0:
|
||||
raise ValueError("speed_range_min must be > 0")
|
||||
raise vol.Invalid("speed_range_min must be > 0")
|
||||
if config[CONF_SPEED_RANGE_MIN] >= config[CONF_SPEED_RANGE_MAX]:
|
||||
raise ValueError("speed_range_max must be > speed_range_min")
|
||||
raise vol.Invalid("speed_range_max must be > speed_range_min")
|
||||
return config
|
||||
|
||||
|
||||
def valid_preset_mode_configuration(config: ConfigType) -> ConfigType:
|
||||
"""Validate that the preset mode reset payload is not one of the preset modes."""
|
||||
if config[CONF_PAYLOAD_RESET_PRESET_MODE] in config[CONF_PRESET_MODES_LIST]:
|
||||
raise ValueError("preset_modes must not contain payload_reset_preset_mode")
|
||||
raise vol.Invalid("preset_modes must not contain payload_reset_preset_mode")
|
||||
return config
|
||||
|
||||
|
||||
|
@@ -102,7 +102,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def valid_mode_configuration(config: ConfigType) -> ConfigType:
|
||||
"""Validate that the mode reset payload is not one of the available modes."""
|
||||
if config[CONF_PAYLOAD_RESET_MODE] in config[CONF_AVAILABLE_MODES_LIST]:
|
||||
raise ValueError("modes must not contain payload_reset_mode")
|
||||
raise vol.Invalid("modes must not contain payload_reset_mode")
|
||||
return config
|
||||
|
||||
|
||||
@@ -113,9 +113,9 @@ def valid_humidity_range_configuration(config: ConfigType) -> ConfigType:
|
||||
throws if it isn't.
|
||||
"""
|
||||
if config[CONF_TARGET_HUMIDITY_MIN] >= config[CONF_TARGET_HUMIDITY_MAX]:
|
||||
raise ValueError("target_humidity_max must be > target_humidity_min")
|
||||
raise vol.Invalid("target_humidity_max must be > target_humidity_min")
|
||||
if config[CONF_TARGET_HUMIDITY_MAX] > 100:
|
||||
raise ValueError("max_humidity must be <= 100")
|
||||
raise vol.Invalid("max_humidity must be <= 100")
|
||||
|
||||
return config
|
||||
|
||||
|
@@ -9,7 +9,6 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol, cast, final
|
||||
|
||||
import voluptuous as vol
|
||||
import yaml
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -28,6 +27,7 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers import (
|
||||
@@ -63,6 +63,7 @@ from homeassistant.helpers.typing import (
|
||||
UndefinedType,
|
||||
)
|
||||
from homeassistant.util.json import json_loads
|
||||
from homeassistant.util.yaml import dump as yaml_dump
|
||||
|
||||
from . import debug_info, subscription
|
||||
from .client import async_publish
|
||||
@@ -207,6 +208,16 @@ def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType
|
||||
)
|
||||
|
||||
|
||||
def validate_sensor_entity_category(config: ConfigType) -> ConfigType:
|
||||
"""Check the sensor's entity category is not set to `config` which is invalid for sensors."""
|
||||
if (
|
||||
CONF_ENTITY_CATEGORY in config
|
||||
and config[CONF_ENTITY_CATEGORY] == EntityCategory.CONFIG
|
||||
):
|
||||
raise vol.Invalid("Entity category `config` is invalid")
|
||||
return config
|
||||
|
||||
|
||||
MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_DEPRECATED_VIA_HUB, CONF_VIA_DEVICE),
|
||||
vol.Schema(
|
||||
@@ -404,8 +415,8 @@ async def async_setup_entity_entry_helper(
|
||||
error = str(ex)
|
||||
config_file = getattr(yaml_config, "__config_file__", "?")
|
||||
line = getattr(yaml_config, "__line__", "?")
|
||||
issue_id = hex(hash(frozenset(yaml_config.items())))
|
||||
yaml_config_str = yaml.dump(dict(yaml_config))
|
||||
issue_id = hex(hash(frozenset(yaml_config)))
|
||||
yaml_config_str = yaml_dump(yaml_config)
|
||||
learn_more_url = (
|
||||
f"https://www.home-assistant.io/integrations/{domain}.mqtt/"
|
||||
)
|
||||
@@ -427,7 +438,7 @@ async def async_setup_entity_entry_helper(
|
||||
translation_key="invalid_platform_config",
|
||||
)
|
||||
_LOGGER.error(
|
||||
"%s for manual configured MQTT %s item, in %s, line %s Got %s",
|
||||
"%s for manually configured MQTT %s item, in %s, line %s Got %s",
|
||||
error,
|
||||
domain,
|
||||
config_file,
|
||||
|
@@ -44,6 +44,7 @@ from .mixins import (
|
||||
MqttAvailability,
|
||||
MqttEntity,
|
||||
async_setup_entity_entry_helper,
|
||||
validate_sensor_entity_category,
|
||||
write_state_on_attr_change,
|
||||
)
|
||||
from .models import (
|
||||
@@ -70,7 +71,6 @@ MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset(
|
||||
DEFAULT_NAME = "MQTT Sensor"
|
||||
DEFAULT_FORCE_UPDATE = False
|
||||
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
@@ -88,6 +88,7 @@ PLATFORM_SCHEMA_MODERN = vol.All(
|
||||
# Deprecated in HA Core 2021.11.0 https://github.com/home-assistant/core/pull/54840
|
||||
# Removed in HA Core 2023.6.0
|
||||
cv.removed(CONF_LAST_RESET_TOPIC),
|
||||
validate_sensor_entity_category,
|
||||
_PLATFORM_SCHEMA_BASE,
|
||||
)
|
||||
|
||||
@@ -95,6 +96,7 @@ DISCOVERY_SCHEMA = vol.All(
|
||||
# Deprecated in HA Core 2021.11.0 https://github.com/home-assistant/core/pull/54840
|
||||
# Removed in HA Core 2023.6.0
|
||||
cv.removed(CONF_LAST_RESET_TOPIC),
|
||||
validate_sensor_entity_category,
|
||||
_PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
|
||||
)
|
||||
|
||||
|
@@ -181,7 +181,7 @@
|
||||
},
|
||||
"qos": {
|
||||
"name": "QoS",
|
||||
"description": "Quality of Service to use. O. At most once. 1: At least once. 2: Exactly once."
|
||||
"description": "Quality of Service to use. 0: At most once. 1: At least once. 2: Exactly once."
|
||||
},
|
||||
"retain": {
|
||||
"name": "Retain",
|
||||
|
@@ -71,9 +71,9 @@ MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset(
|
||||
def valid_text_size_configuration(config: ConfigType) -> ConfigType:
|
||||
"""Validate that the text length configuration is valid, throws if it isn't."""
|
||||
if config[CONF_MIN] >= config[CONF_MAX]:
|
||||
raise ValueError("text length min must be >= max")
|
||||
raise vol.Invalid("text length min must be >= max")
|
||||
if config[CONF_MAX] > MAX_LENGTH_STATE_STATE:
|
||||
raise ValueError(f"max text length must be <= {MAX_LENGTH_STATE_STATE}")
|
||||
raise vol.Invalid(f"max text length must be <= {MAX_LENGTH_STATE_STATE}")
|
||||
|
||||
return config
|
||||
|
||||
|
@@ -47,7 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
).extend(mqtt.config.MQTT_RO_SCHEMA.schema)
|
||||
).extend(mqtt.MQTT_RO_SCHEMA.schema)
|
||||
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["google_nest_sdm"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["google-nest-sdm==3.0.2"]
|
||||
"requirements": ["google-nest-sdm==3.0.3"]
|
||||
}
|
||||
|
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyatmo"],
|
||||
"requirements": ["pyatmo==7.5.0"]
|
||||
"requirements": ["pyatmo==7.6.0"]
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nextbus",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["py_nextbus"],
|
||||
"requirements": ["py-nextbusnext==1.0.0"]
|
||||
"requirements": ["py-nextbusnext==1.0.2"]
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from aiohttp import BasicAuth
|
||||
from python_opensky import OpenSky
|
||||
from python_opensky.exceptions import OpenSkyUnauthenticatedError
|
||||
from python_opensky.exceptions import OpenSkyError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
@@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
),
|
||||
contributing_user=entry.options.get(CONF_CONTRIBUTING_USER, False),
|
||||
)
|
||||
except OpenSkyUnauthenticatedError as exc:
|
||||
except OpenSkyError as exc:
|
||||
raise ConfigEntryNotReady from exc
|
||||
|
||||
coordinator = OpenSkyDataUpdateCoordinator(hass, client)
|
||||
|
@@ -23,7 +23,7 @@ from homeassistant.components.recorder.statistics import (
|
||||
statistics_during_period,
|
||||
)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
@@ -58,6 +58,16 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
entry_data.get(CONF_TOTP_SECRET),
|
||||
)
|
||||
|
||||
@callback
|
||||
def _dummy_listener() -> None:
|
||||
pass
|
||||
|
||||
# Force the coordinator to periodically update by registering at least one listener.
|
||||
# Needed when the _async_update_data below returns {} for utilities that don't provide
|
||||
# forecast, which results to no sensors added, no registered listeners, and thus
|
||||
# _async_update_data not periodically getting called which is needed for _insert_statistics.
|
||||
self.async_add_listener(_dummy_listener)
|
||||
|
||||
async def _async_update_data(
|
||||
self,
|
||||
) -> dict[str, Forecast]:
|
||||
@@ -71,6 +81,8 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
raise ConfigEntryAuthFailed from err
|
||||
forecasts: list[Forecast] = await self.api.async_get_forecast()
|
||||
_LOGGER.debug("Updating sensor data with: %s", forecasts)
|
||||
# Because Opower provides historical usage/cost with a delay of a couple of days
|
||||
# we need to insert data into statistics.
|
||||
await self._insert_statistics()
|
||||
return {forecast.account.utility_account_id: forecast for forecast in forecasts}
|
||||
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.0.37"]
|
||||
"requirements": ["opower==0.0.39"]
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@ SELECT_TYPES = (
|
||||
key="select_schedule",
|
||||
translation_key="select_schedule",
|
||||
icon="mdi:calendar-clock",
|
||||
command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON),
|
||||
command=lambda api, loc, opt: api.set_schedule_state(loc, STATE_ON, opt),
|
||||
options_key="available_schedules",
|
||||
),
|
||||
PlugwiseSelectEntityDescription(
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/private_ble_device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluetooth-data-tools==1.13.0"]
|
||||
"requirements": ["bluetooth-data-tools==1.14.0"]
|
||||
}
|
||||
|
@@ -70,25 +70,25 @@ def async_setup_proximity_component(
|
||||
ignored_zones: list[str] = config[CONF_IGNORED_ZONES]
|
||||
proximity_devices: list[str] = config[CONF_DEVICES]
|
||||
tolerance: int = config[CONF_TOLERANCE]
|
||||
proximity_zone = name
|
||||
proximity_zone = config[CONF_ZONE]
|
||||
unit_of_measurement: str = config.get(
|
||||
CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit
|
||||
)
|
||||
zone_id = f"zone.{config[CONF_ZONE]}"
|
||||
zone_friendly_name = name
|
||||
|
||||
proximity = Proximity(
|
||||
hass,
|
||||
proximity_zone,
|
||||
zone_friendly_name,
|
||||
DEFAULT_DIST_TO_ZONE,
|
||||
DEFAULT_DIR_OF_TRAVEL,
|
||||
DEFAULT_NEAREST,
|
||||
ignored_zones,
|
||||
proximity_devices,
|
||||
tolerance,
|
||||
zone_id,
|
||||
proximity_zone,
|
||||
unit_of_measurement,
|
||||
)
|
||||
proximity.entity_id = f"{DOMAIN}.{proximity_zone}"
|
||||
proximity.entity_id = f"{DOMAIN}.{zone_friendly_name}"
|
||||
|
||||
proximity.async_write_ha_state()
|
||||
|
||||
@@ -171,7 +171,7 @@ class Proximity(Entity):
|
||||
devices_to_calculate = False
|
||||
devices_in_zone = ""
|
||||
|
||||
zone_state = self.hass.states.get(self.proximity_zone)
|
||||
zone_state = self.hass.states.get(f"zone.{self.proximity_zone}")
|
||||
proximity_latitude = (
|
||||
zone_state.attributes.get(ATTR_LATITUDE) if zone_state else None
|
||||
)
|
||||
@@ -189,7 +189,7 @@ class Proximity(Entity):
|
||||
devices_to_calculate = True
|
||||
|
||||
# Check the location of all devices.
|
||||
if (device_state.state).lower() == (self.friendly_name).lower():
|
||||
if (device_state.state).lower() == (self.proximity_zone).lower():
|
||||
device_friendly = device_state.name
|
||||
if devices_in_zone != "":
|
||||
devices_in_zone = f"{devices_in_zone}, "
|
||||
|
@@ -10,6 +10,7 @@ from typing import Literal
|
||||
|
||||
from reolink_aio.api import RETRY_ATTEMPTS
|
||||
from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError
|
||||
from reolink_aio.software_version import NewSoftwareVersion
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||
@@ -45,7 +46,9 @@ class ReolinkData:
|
||||
|
||||
host: ReolinkHost
|
||||
device_coordinator: DataUpdateCoordinator[None]
|
||||
firmware_coordinator: DataUpdateCoordinator[str | Literal[False]]
|
||||
firmware_coordinator: DataUpdateCoordinator[
|
||||
str | Literal[False] | NewSoftwareVersion
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
@@ -86,7 +89,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
|
||||
await host.renew()
|
||||
|
||||
async def async_check_firmware_update() -> str | Literal[False]:
|
||||
async def async_check_firmware_update() -> str | Literal[
|
||||
False
|
||||
] | NewSoftwareVersion:
|
||||
"""Check for firmware updates."""
|
||||
if not host.api.supported(None, "update"):
|
||||
return False
|
||||
@@ -153,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
return True
|
||||
|
||||
|
||||
async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Update the configuration of the host entity."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
@@ -32,7 +32,7 @@ from .entity import ReolinkChannelCoordinatorEntity
|
||||
class ReolinkBinarySensorEntityDescriptionMixin:
|
||||
"""Mixin values for Reolink binary sensor entities."""
|
||||
|
||||
value: Callable[[Host, int | None], bool]
|
||||
value: Callable[[Host, int], bool]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -43,7 +43,7 @@ class ReolinkBinarySensorEntityDescription(
|
||||
|
||||
icon: str = "mdi:motion-sensor"
|
||||
icon_off: str = "mdi:motion-sensor-off"
|
||||
supported: Callable[[Host, int | None], bool] = lambda host, ch: True
|
||||
supported: Callable[[Host, int], bool] = lambda host, ch: True
|
||||
|
||||
|
||||
BINARY_SENSORS = (
|
||||
@@ -169,6 +169,6 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt
|
||||
)
|
||||
)
|
||||
|
||||
async def _async_handle_event(self, event):
|
||||
async def _async_handle_event(self, event: str) -> None:
|
||||
"""Handle incoming event for motion detection."""
|
||||
self.async_write_ha_state()
|
||||
|
@@ -113,7 +113,9 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
raise AbortFlow("already_configured")
|
||||
|
||||
# check if the camera is reachable at the new IP
|
||||
host = ReolinkHost(self.hass, existing_entry.data, existing_entry.options)
|
||||
new_config = dict(existing_entry.data)
|
||||
new_config[CONF_HOST] = discovery_info.ip
|
||||
host = ReolinkHost(self.hass, new_config, existing_entry.options)
|
||||
try:
|
||||
await host.api.get_state("GetLocalLink")
|
||||
await host.api.logout()
|
||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.web import Request
|
||||
@@ -81,7 +81,7 @@ class ReolinkHost:
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def api(self):
|
||||
def api(self) -> Host:
|
||||
"""Return the API object."""
|
||||
return self._api
|
||||
|
||||
@@ -313,7 +313,7 @@ class ReolinkHost:
|
||||
"""Call the API of the camera device to update the internal states."""
|
||||
await self._api.get_states()
|
||||
|
||||
async def disconnect(self):
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from the API, so the connection will be released."""
|
||||
try:
|
||||
await self._api.unsubscribe()
|
||||
@@ -335,7 +335,7 @@ class ReolinkHost:
|
||||
err,
|
||||
)
|
||||
|
||||
async def _async_start_long_polling(self, initial=False):
|
||||
async def _async_start_long_polling(self, initial=False) -> None:
|
||||
"""Start ONVIF long polling task."""
|
||||
if self._long_poll_task is None:
|
||||
try:
|
||||
@@ -364,7 +364,7 @@ class ReolinkHost:
|
||||
self._lost_subscription = False
|
||||
self._long_poll_task = asyncio.create_task(self._async_long_polling())
|
||||
|
||||
async def _async_stop_long_polling(self):
|
||||
async def _async_stop_long_polling(self) -> None:
|
||||
"""Stop ONVIF long polling task."""
|
||||
if self._long_poll_task is not None:
|
||||
self._long_poll_task.cancel()
|
||||
@@ -372,7 +372,7 @@ class ReolinkHost:
|
||||
|
||||
await self._api.unsubscribe(sub_type=SubType.long_poll)
|
||||
|
||||
async def stop(self, event=None):
|
||||
async def stop(self, event=None) -> None:
|
||||
"""Disconnect the API."""
|
||||
if self._cancel_poll is not None:
|
||||
self._cancel_poll()
|
||||
@@ -433,7 +433,7 @@ class ReolinkHost:
|
||||
else:
|
||||
self._lost_subscription = False
|
||||
|
||||
async def _renew(self, sub_type: SubType) -> None:
|
||||
async def _renew(self, sub_type: Literal[SubType.push, SubType.long_poll]) -> None:
|
||||
"""Execute the renew of the subscription."""
|
||||
if not self._api.subscribed(sub_type):
|
||||
_LOGGER.debug(
|
||||
@@ -512,8 +512,10 @@ class ReolinkHost:
|
||||
|
||||
_LOGGER.debug("Registered webhook: %s", event_id)
|
||||
|
||||
def unregister_webhook(self):
|
||||
def unregister_webhook(self) -> None:
|
||||
"""Unregister the webhook for motion events."""
|
||||
if self.webhook_id is None:
|
||||
return
|
||||
_LOGGER.debug("Unregistering webhook %s", self.webhook_id)
|
||||
webhook.async_unregister(self._hass, self.webhook_id)
|
||||
self.webhook_id = None
|
||||
|
@@ -38,8 +38,8 @@ class ReolinkLightEntityDescription(
|
||||
"""A class that describes light entities."""
|
||||
|
||||
supported_fn: Callable[[Host, int], bool] = lambda api, ch: True
|
||||
get_brightness_fn: Callable[[Host, int], int] | None = None
|
||||
set_brightness_fn: Callable[[Host, int, float], Any] | None = None
|
||||
get_brightness_fn: Callable[[Host, int], int | None] | None = None
|
||||
set_brightness_fn: Callable[[Host, int, int], Any] | None = None
|
||||
|
||||
|
||||
LIGHT_ENTITIES = (
|
||||
@@ -127,13 +127,13 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
|
||||
if self.entity_description.get_brightness_fn is None:
|
||||
return None
|
||||
|
||||
return round(
|
||||
255
|
||||
* (
|
||||
self.entity_description.get_brightness_fn(self._host.api, self._channel)
|
||||
/ 100.0
|
||||
)
|
||||
bright_pct = self.entity_description.get_brightness_fn(
|
||||
self._host.api, self._channel
|
||||
)
|
||||
if bright_pct is None:
|
||||
return None
|
||||
|
||||
return round(255 * bright_pct / 100.0)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn light off."""
|
||||
|
@@ -18,5 +18,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"requirements": ["reolink-aio==0.7.11"]
|
||||
"requirements": ["reolink-aio==0.7.14"]
|
||||
}
|
||||
|
@@ -26,7 +26,7 @@ from .entity import ReolinkChannelCoordinatorEntity
|
||||
class ReolinkNumberEntityDescriptionMixin:
|
||||
"""Mixin values for Reolink number entities."""
|
||||
|
||||
value: Callable[[Host, int], float]
|
||||
value: Callable[[Host, int], float | None]
|
||||
method: Callable[[Host, int, float], Any]
|
||||
|
||||
|
||||
@@ -354,7 +354,7 @@ class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
def native_value(self) -> float | None:
|
||||
"""State of the number entity."""
|
||||
return self.entity_description.value(self._host.api, self._channel)
|
||||
|
||||
|
@@ -44,7 +44,7 @@ class ReolinkSensorEntityDescription(
|
||||
class ReolinkHostSensorEntityDescriptionMixin:
|
||||
"""Mixin values for Reolink host sensor entities."""
|
||||
|
||||
value: Callable[[Host], int]
|
||||
value: Callable[[Host], int | None]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@@ -35,7 +35,8 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class ReolinkUpdateEntity(
|
||||
ReolinkBaseCoordinatorEntity[str | Literal[False]], UpdateEntity
|
||||
ReolinkBaseCoordinatorEntity[str | Literal[False] | NewSoftwareVersion],
|
||||
UpdateEntity,
|
||||
):
|
||||
"""Update entity for a Netgear device."""
|
||||
|
||||
|
@@ -119,7 +119,7 @@ class IRobotEntity(Entity):
|
||||
@property
|
||||
def battery_stats(self):
|
||||
"""Return the battery stats."""
|
||||
return self.vacuum_state.get("bbchg3")
|
||||
return self.vacuum_state.get("bbchg3", {})
|
||||
|
||||
@property
|
||||
def _robot_state(self):
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user