forked from home-assistant/core
Compare commits
347 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ee39275fc | |||
| 728399c09a | |||
| c76c66c466 | |||
| 2f8d8eed84 | |||
| a372878aed | |||
| f5d7e6f506 | |||
| 2cf87b8536 | |||
| 68450bb594 | |||
| 4f5c808b40 | |||
| 4eb1bac859 | |||
| f6bef0081b | |||
| f590f86cdd | |||
| 85ad0a0e1e | |||
| bd08807b53 | |||
| 715c6a554c | |||
| e3074c6148 | |||
| b7c97880e9 | |||
| 6745814e59 | |||
| adf24c970c | |||
| f13052f9aa | |||
| 2a12369e31 | |||
| 76ec26b23d | |||
| d85a92f5f1 | |||
| ac74eb7854 | |||
| 8769921d70 | |||
| 54c5c94f97 | |||
| 9cb425ae8b | |||
| eff82ba82c | |||
| f21313b622 | |||
| bba7641c77 | |||
| 7aa14e20d1 | |||
| b55b2c8da3 | |||
| 8c05ebd031 | |||
| 34a3e88e0d | |||
| bf002ac0b0 | |||
| 6f529a2c77 | |||
| e5db7278e1 | |||
| cdf67e9bb5 | |||
| 393359a546 | |||
| 9309e38302 | |||
| 479ecc8b94 | |||
| ec7950aeda | |||
| c763483049 | |||
| fe84e7a576 | |||
| 5ba31290b8 | |||
| de619e4ddc | |||
| 56ceadaeeb | |||
| da61564f82 | |||
| 003673cd29 | |||
| da6c571e65 | |||
| 159fab7025 | |||
| 96a10e76b8 | |||
| e7068ae134 | |||
| 6a0c3f1b4f | |||
| a0ae18a1b6 | |||
| ad761bb2de | |||
| edb69fb095 | |||
| 58b28e6df1 | |||
| 973a13abfa | |||
| 2a51377cef | |||
| 87bd67656b | |||
| c79bc17d17 | |||
| 54270df217 | |||
| 5a87cde71e | |||
| e825bcc282 | |||
| b54a3170f0 | |||
| 349d8f5c28 | |||
| cfd1f7809f | |||
| 5f9cc2fec1 | |||
| 58d46f6dec | |||
| 74ea9e24df | |||
| 437a2a829f | |||
| f5884c6279 | |||
| e4382a494c | |||
| 56ff767969 | |||
| 4a18f592c6 | |||
| 7ff2f376d4 | |||
| a18918bb73 | |||
| 49e5709826 | |||
| c665903f9d | |||
| de44af2948 | |||
| 95a800b6bc | |||
| a9e9ec2c3d | |||
| 7309c3c290 | |||
| f48d70654b | |||
| a9b3c2e2b5 | |||
| 19349e1779 | |||
| e320d715c7 | |||
| 44c9ea68eb | |||
| dbfee24eb7 | |||
| 3b7271d597 | |||
| 9dbf84228e | |||
| 9e47d03086 | |||
| f63aaf8b5a | |||
| 8375fc235d | |||
| 3030870de0 | |||
| f61c70b686 | |||
| e720b398d2 | |||
| bd21490a57 | |||
| 75b308c1aa | |||
| 881707e1fe | |||
| 2ca3bbaea5 | |||
| ea4bdbb3a0 | |||
| 40cfc31dcb | |||
| 031aadff00 | |||
| 27691b7d48 | |||
| fe94107af7 | |||
| d784a76d32 | |||
| ebb1912617 | |||
| 8c605c29c3 | |||
| 74a75e709f | |||
| 2103875ff7 | |||
| 5c83b774bb | |||
| 2c870f9da9 | |||
| 40adb3809f | |||
| 8aa1242221 | |||
| 8569ddc5f9 | |||
| 7032415528 | |||
| d099fb2a26 | |||
| 35fad52913 | |||
| c170132827 | |||
| 439f82a4ec | |||
| 2481d14632 | |||
| 3cf826dc93 | |||
| e25ddf9650 | |||
| 8d79ac67f5 | |||
| 5025c15165 | |||
| ffd5e04a29 | |||
| 9fcdfd1b16 | |||
| c1e5b2e6cc | |||
| 31c0d21204 | |||
| 3ba63fc78f | |||
| 0395315267 | |||
| 6b354457c2 | |||
| df88335370 | |||
| 4c6c5ee63d | |||
| 65476914ed | |||
| d30a2e3611 | |||
| eb510e3630 | |||
| 532df5b5f1 | |||
| 1534f99c80 | |||
| a19aa9595a | |||
| e3191d098f | |||
| cc36071612 | |||
| 2d90ee8237 | |||
| 16266703df | |||
| dd2cc52119 | |||
| c48c8c25fa | |||
| 83a5659d57 | |||
| 3183cd346d | |||
| 5930c841d7 | |||
| f05ba22b5c | |||
| ecc6cc280a | |||
| f50afd6004 | |||
| 44ecaa740b | |||
| 91b1a8e962 | |||
| 74f1b18b73 | |||
| 8ab1c044bd | |||
| 66d8856033 | |||
| 5c1e4379a9 | |||
| bca9826e18 | |||
| 3a067d445d | |||
| 4d7c96205d | |||
| 3934524d4a | |||
| 5747f8ce9d | |||
| 02ebf1d7f8 | |||
| d789e83879 | |||
| ce29b4a7e3 | |||
| e2695ba88f | |||
| 7ca83a7648 | |||
| c0efec4a84 | |||
| f766fbfb98 | |||
| bd78c44ac5 | |||
| fd2469e2a7 | |||
| a7010e3e80 | |||
| d379a9aaae | |||
| 2dc630a4af | |||
| ba0c065750 | |||
| 4fca06256b | |||
| 0a25788822 | |||
| 94e1eaa15d | |||
| dfc26e4509 | |||
| b14add5914 | |||
| 7be6aa455e | |||
| 40636f2273 | |||
| f91f98e309 | |||
| 838b1338b8 | |||
| d3aa7375f0 | |||
| 514ce59a8f | |||
| 25063821e1 | |||
| b3c257fb79 | |||
| cb02c2e6d0 | |||
| 1000fae905 | |||
| 30b9a28502 | |||
| 42bf086c97 | |||
| 064f412da4 | |||
| 122652b396 | |||
| 32b25c7e53 | |||
| 83c487a319 | |||
| 75c0c7bda0 | |||
| 4d7abbf8c5 | |||
| bf4bc9d935 | |||
| e1699b4d65 | |||
| 67362db547 | |||
| dafdcd369c | |||
| 384070c158 | |||
| 46016004fa | |||
| 38fcc88c57 | |||
| 9cde864224 | |||
| 439f1766a0 | |||
| 12e32fb799 | |||
| 14ad2e91f3 | |||
| 8c0cd6bbab | |||
| c02e96c5c0 | |||
| 3a08e3bec6 | |||
| a62c05b983 | |||
| 79846d5668 | |||
| 482032cb87 | |||
| 9ebf985010 | |||
| f16c0bd559 | |||
| 1df5ad23ef | |||
| d99ba75ed8 | |||
| ceeef1eacc | |||
| 856780ed30 | |||
| 500b0a9b52 | |||
| 855edba3a2 | |||
| e7203d6015 | |||
| 97446a5af3 | |||
| ac2e05b5c0 | |||
| af07ac120e | |||
| f2de666c54 | |||
| 38288dd68e | |||
| 3e2f97d105 | |||
| 5f014f42ac | |||
| 4b2adab24d | |||
| ef6fed5067 | |||
| 0a627aed6d | |||
| cb03a6e29b | |||
| e630027455 | |||
| 280d7ef4ec | |||
| 93b7ffa807 | |||
| cc8e9ac141 | |||
| 2724b115da | |||
| 150fb151fa | |||
| 4b8cb35ba0 | |||
| 5cce878b85 | |||
| 92ebc5b436 | |||
| 463320c8ee | |||
| 3bf5fa9302 | |||
| 650ab70444 | |||
| bb8a74a3f4 | |||
| 49445c46a0 | |||
| 490101fa92 | |||
| 2ac4bb8e9f | |||
| 1c0a6970e2 | |||
| f3f69a8107 | |||
| a77bcccbbb | |||
| e98da8596a | |||
| 5843c93371 | |||
| 5991b06574 | |||
| fecdfbfb9f | |||
| 8cfe3821da | |||
| cba67d1525 | |||
| 39df394414 | |||
| db91a40b55 | |||
| 3181358484 | |||
| 776e2da4e6 | |||
| 72ffdf4f4b | |||
| 36bba95fd0 | |||
| 13fc69d8a8 | |||
| 94464f220c | |||
| 13decb9b10 | |||
| b6226acd2b | |||
| d04282a41c | |||
| a671d0bc6c | |||
| 6c84e8dff0 | |||
| 57279b1c7b | |||
| a5646e0df2 | |||
| 63ad3ebdf4 | |||
| f2a9ef6591 | |||
| 2a5bb66c06 | |||
| 0c0be6d6a1 | |||
| e99a58ad53 | |||
| 6c4b773bc1 | |||
| c571f36c6c | |||
| f1b041afbe | |||
| 183af92658 | |||
| d71dd12263 | |||
| 18a7aa20b4 | |||
| f8fde71ef3 | |||
| 966798b588 | |||
| 66b6f81996 | |||
| 4a1a5b9e87 | |||
| f01f033b3f | |||
| cd884de79e | |||
| 14be7e4a72 | |||
| 2be71d53c5 | |||
| 1c960d300d | |||
| 87a1482e4d | |||
| 24b8e60978 | |||
| 404e30911b | |||
| 9c599f7513 | |||
| f3e8360949 | |||
| 43f8731f8a | |||
| 4a60d36216 | |||
| 07a1ee0baa | |||
| a57c30be88 | |||
| b1bf69689e | |||
| 4735aadaa8 | |||
| f18f161efa | |||
| 3c90e3d83f | |||
| 57a43ef151 | |||
| 6afc6ca126 | |||
| 92a3edc536 | |||
| e91e67b400 | |||
| 7a7bcf1a92 | |||
| 1383a0c13a | |||
| 12e3077895 | |||
| 05c0973937 | |||
| 7467d588c8 | |||
| 0c82e0a618 | |||
| 5a4f88349a | |||
| e720e82fd6 | |||
| 8ed0af2fb7 | |||
| 550c0bf3c3 | |||
| 8bb98b4146 | |||
| bd5bc6b83d | |||
| a2b6b0a0bc | |||
| 41ad3d8987 | |||
| 3e7dc3588d | |||
| 66d802b5e5 | |||
| f77bd13cc0 | |||
| fe4ad30ade | |||
| 15a1a4bfdf | |||
| 3d80c4f7f6 | |||
| 0015af0b3c | |||
| a535bda821 | |||
| ca539630a6 | |||
| faf2a90cd1 | |||
| 6aba79d7b9 | |||
| b464e77112 | |||
| a8b39ce332 | |||
| 77b25553e3 | |||
| c31dfd6d00 | |||
| 647ac10dd9 | |||
| 50dfe4dec0 | |||
| 52a8216150 |
@@ -485,6 +485,7 @@ omit =
|
||||
homeassistant/components/gpsd/sensor.py
|
||||
homeassistant/components/greenwave/light.py
|
||||
homeassistant/components/growatt_server/__init__.py
|
||||
homeassistant/components/growatt_server/const.py
|
||||
homeassistant/components/growatt_server/sensor.py
|
||||
homeassistant/components/growatt_server/sensor_types/*
|
||||
homeassistant/components/gstreamer/media_player.py
|
||||
|
||||
@@ -786,8 +786,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/media_source/ @hunterjm
|
||||
/tests/components/media_source/ @hunterjm
|
||||
/homeassistant/components/mediaroom/ @dgomes
|
||||
/homeassistant/components/melcloud/ @vilppuvuorinen
|
||||
/tests/components/melcloud/ @vilppuvuorinen
|
||||
/homeassistant/components/melissa/ @kennedyshead
|
||||
/tests/components/melissa/ @kennedyshead
|
||||
/homeassistant/components/melnor/ @vanstinator
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.01.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.01.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.01.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.01.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.01.0
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.02.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.02.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.02.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.02.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.02.0
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "tplink",
|
||||
"name": "TP-Link",
|
||||
"integrations": ["tplink", "tplink_omada", "tplink_lte"],
|
||||
"integrations": ["tplink", "tplink_omada", "tplink_lte", "tplink_tapo"],
|
||||
"iot_standards": ["matter"]
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ class AdaxDevice(ClimateEntity):
|
||||
)
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None:
|
||||
"""Initialize the heater."""
|
||||
|
||||
@@ -83,6 +83,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
||||
_attr_max_temp = 32
|
||||
_attr_min_temp = 16
|
||||
_attr_name = None
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
|
||||
"""Initialize an AdvantageAir AC unit."""
|
||||
@@ -202,11 +203,16 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity):
|
||||
"""AdvantageAir MyTemp Zone control."""
|
||||
|
||||
_attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT_COOL]
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_max_temp = 32
|
||||
_attr_min_temp = 16
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||
"""Initialize an AdvantageAir Zone control."""
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airthings-ble==0.6.0"]
|
||||
"requirements": ["airthings-ble==0.6.1"]
|
||||
}
|
||||
|
||||
@@ -88,9 +88,13 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
|
||||
_attr_name = None
|
||||
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, coordinator, ac_number, info):
|
||||
"""Initialize the climate device."""
|
||||
@@ -192,9 +196,14 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_hvac_modes = AT_GROUP_MODES
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, coordinator, group_number, info):
|
||||
"""Initialize the climate device."""
|
||||
|
||||
@@ -120,15 +120,12 @@ class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity):
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_target_temperature_step = 1
|
||||
_attr_name = None
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
|
||||
class Airtouch5AC(Airtouch5ClimateEntity):
|
||||
"""Representation of the AC unit. Used to control the overall HVAC Mode."""
|
||||
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
|
||||
)
|
||||
|
||||
def __init__(self, client: Airtouch5SimpleClient, ability: AcAbility) -> None:
|
||||
"""Initialise the Climate Entity."""
|
||||
super().__init__(client)
|
||||
@@ -152,6 +149,14 @@ class Airtouch5AC(Airtouch5ClimateEntity):
|
||||
if ability.supports_mode_heat:
|
||||
self._attr_hvac_modes.append(HVACMode.HEAT)
|
||||
|
||||
self._attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
|
||||
)
|
||||
if len(self.hvac_modes) > 1:
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
self._attr_fan_modes = []
|
||||
if ability.supports_fan_speed_quiet:
|
||||
self._attr_fan_modes.append(FAN_DIFFUSE)
|
||||
@@ -262,7 +267,10 @@ class Airtouch5Zone(Airtouch5ClimateEntity):
|
||||
_attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY]
|
||||
_attr_preset_modes = [PRESET_NONE, PRESET_BOOST]
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -117,6 +117,7 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
_attr_name = None
|
||||
_speeds: dict[int, str] = {}
|
||||
_speeds_reverse: dict[str, int] = {}
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -129,7 +130,11 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
super().__init__(coordinator, entry, system_zone_id, zone_data)
|
||||
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{system_zone_id}"
|
||||
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
self._attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
self._attr_target_temperature_step = API_TEMPERATURE_STEP
|
||||
self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[
|
||||
self.get_airzone_value(AZD_TEMP_UNIT)
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.7.2"]
|
||||
"requirements": ["aioairzone==0.7.4"]
|
||||
}
|
||||
|
||||
@@ -144,8 +144,8 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity):
|
||||
"""Define an Airzone Cloud climate."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
@@ -175,6 +175,12 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity):
|
||||
class AirzoneDeviceClimate(AirzoneClimate):
|
||||
"""Define an Airzone Cloud Device base class."""
|
||||
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
params = {
|
||||
@@ -212,6 +218,12 @@ class AirzoneDeviceClimate(AirzoneClimate):
|
||||
class AirzoneDeviceGroupClimate(AirzoneClimate):
|
||||
"""Define an Airzone Cloud DeviceGroup base class."""
|
||||
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
params = {
|
||||
|
||||
@@ -3,18 +3,46 @@ from __future__ import annotations
|
||||
|
||||
import amberelectric
|
||||
from amberelectric.api import amber_api
|
||||
from amberelectric.model.site import Site
|
||||
from amberelectric.model.site import Site, SiteStatus
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, CONF_SITE_NMI, DOMAIN
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN
|
||||
|
||||
API_URL = "https://app.amber.com.au/developers"
|
||||
|
||||
|
||||
def generate_site_selector_name(site: Site) -> str:
|
||||
"""Generate the name to show in the site drop down in the configuration flow."""
|
||||
if site.status == SiteStatus.CLOSED:
|
||||
return site.nmi + " (Closed: " + site.closed_on.isoformat() + ")" # type: ignore[no-any-return]
|
||||
if site.status == SiteStatus.PENDING:
|
||||
return site.nmi + " (Pending)" # type: ignore[no-any-return]
|
||||
return site.nmi # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def filter_sites(sites: list[Site]) -> list[Site]:
|
||||
"""Deduplicates the list of sites."""
|
||||
filtered: list[Site] = []
|
||||
filtered_nmi: set[str] = set()
|
||||
|
||||
for site in sorted(sites, key=lambda site: site.status.value):
|
||||
if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi:
|
||||
filtered.append(site)
|
||||
filtered_nmi.add(site.nmi)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
|
||||
@@ -31,7 +59,7 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
api: amber_api.AmberApi = amber_api.AmberApi.create(configuration)
|
||||
|
||||
try:
|
||||
sites: list[Site] = api.get_sites()
|
||||
sites: list[Site] = filter_sites(api.get_sites())
|
||||
if len(sites) == 0:
|
||||
self._errors[CONF_API_TOKEN] = "no_site"
|
||||
return None
|
||||
@@ -86,38 +114,31 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
assert self._sites is not None
|
||||
assert self._api_token is not None
|
||||
|
||||
api_token = self._api_token
|
||||
if user_input is not None:
|
||||
site_nmi = user_input[CONF_SITE_NMI]
|
||||
sites = [site for site in self._sites if site.nmi == site_nmi]
|
||||
site = sites[0]
|
||||
site_id = site.id
|
||||
site_id = user_input[CONF_SITE_ID]
|
||||
name = user_input.get(CONF_SITE_NAME, site_id)
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={
|
||||
CONF_SITE_ID: site_id,
|
||||
CONF_API_TOKEN: api_token,
|
||||
CONF_SITE_NMI: site.nmi,
|
||||
},
|
||||
data={CONF_SITE_ID: site_id, CONF_API_TOKEN: self._api_token},
|
||||
)
|
||||
|
||||
user_input = {
|
||||
CONF_API_TOKEN: api_token,
|
||||
CONF_SITE_NMI: "",
|
||||
CONF_SITE_NAME: "",
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="site",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_SITE_NMI, default=user_input[CONF_SITE_NMI]
|
||||
): vol.In([site.nmi for site in self._sites]),
|
||||
vol.Optional(
|
||||
CONF_SITE_NAME, default=user_input[CONF_SITE_NAME]
|
||||
): str,
|
||||
vol.Required(CONF_SITE_ID): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
SelectOptionDict(
|
||||
value=site.id,
|
||||
label=generate_site_selector_name(site),
|
||||
)
|
||||
for site in self._sites
|
||||
],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_SITE_NAME): str,
|
||||
}
|
||||
),
|
||||
errors=self._errors,
|
||||
|
||||
@@ -6,7 +6,6 @@ from homeassistant.const import Platform
|
||||
DOMAIN = "amberelectric"
|
||||
CONF_SITE_NAME = "site_name"
|
||||
CONF_SITE_ID = "site_id"
|
||||
CONF_SITE_NMI = "site_nmi"
|
||||
|
||||
ATTRIBUTION = "Data provided by Amber Electric"
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["amberelectric"],
|
||||
"requirements": ["amberelectric==1.0.4"]
|
||||
"requirements": ["amberelectric==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -153,10 +153,15 @@ class AmbiclimateEntity(ClimateEntity):
|
||||
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_target_temperature_step = 1
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, heater: AmbiclimateDevice, store: Store[dict[str, Any]]) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
@@ -3,11 +3,15 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from python_homeassistant_analytics import HomeassistantAnalyticsClient
|
||||
from python_homeassistant_analytics import (
|
||||
HomeassistantAnalyticsClient,
|
||||
HomeassistantAnalyticsConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_TRACKED_INTEGRATIONS, DOMAIN
|
||||
@@ -28,7 +32,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Homeassistant Analytics from a config entry."""
|
||||
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))
|
||||
|
||||
integrations = await client.get_integrations()
|
||||
try:
|
||||
integrations = await client.get_integrations()
|
||||
except HomeassistantAnalyticsConnectionError as ex:
|
||||
raise ConfigEntryNotReady("Could not fetch integration list") from ex
|
||||
|
||||
names = {}
|
||||
for integration in entry.options[CONF_TRACKED_INTEGRATIONS]:
|
||||
|
||||
@@ -53,10 +53,25 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
self._async_abort_entries_match()
|
||||
if user_input:
|
||||
return self.async_create_entry(
|
||||
title="Home Assistant Analytics Insights", data={}, options=user_input
|
||||
)
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS
|
||||
):
|
||||
errors["base"] = "no_integrations_selected"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="Home Assistant Analytics Insights",
|
||||
data={},
|
||||
options={
|
||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_INTEGRATIONS, []
|
||||
),
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS, []
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
client = HomeassistantAnalyticsClient(
|
||||
session=async_get_clientsession(self.hass)
|
||||
@@ -78,16 +93,17 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
]
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=options,
|
||||
multiple=True,
|
||||
sort=True,
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
|
||||
vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(custom_integrations),
|
||||
multiple=True,
|
||||
@@ -106,8 +122,24 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS
|
||||
):
|
||||
errors["base"] = "no_integrations_selected"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="",
|
||||
data={
|
||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_INTEGRATIONS, []
|
||||
),
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS, []
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
client = HomeassistantAnalyticsClient(
|
||||
session=async_get_clientsession(self.hass)
|
||||
@@ -129,17 +161,18 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
]
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
errors=errors,
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=options,
|
||||
multiple=True,
|
||||
sort=True,
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
|
||||
vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(custom_integrations),
|
||||
multiple=True,
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -93,6 +94,7 @@ class HomeassistantAnalyticsSensor(
|
||||
"""Home Assistant Analytics Sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
entity_description: AnalyticsSensorEntityDescription
|
||||
|
||||
|
||||
@@ -3,25 +3,41 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"tracked_integrations": "Integrations"
|
||||
"tracked_integrations": "Integrations",
|
||||
"tracked_custom_integrations": "Custom integrations"
|
||||
},
|
||||
"data_description": {
|
||||
"tracked_integrations": "Select the integrations you want to track",
|
||||
"tracked_custom_integrations": "Select the custom integrations you want to track"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
},
|
||||
"error": {
|
||||
"no_integration_selected": "You must select at least one integration to track"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]"
|
||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
|
||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
|
||||
},
|
||||
"data_description": {
|
||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
|
||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"error": {
|
||||
"no_integration_selected": "[%key:component::analytics_insights::config::error::no_integration_selected%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["py-aosmith==1.0.6"]
|
||||
"requirements": ["py-aosmith==1.0.8"]
|
||||
}
|
||||
|
||||
@@ -52,9 +52,11 @@ def get_service(
|
||||
return None
|
||||
|
||||
# Ordered list of URLs
|
||||
if config.get(CONF_URL) and not a_obj.add(config[CONF_URL]):
|
||||
_LOGGER.error("Invalid Apprise URL(s) supplied")
|
||||
return None
|
||||
if urls := config.get(CONF_URL):
|
||||
for entry in urls:
|
||||
if not a_obj.add(entry):
|
||||
_LOGGER.error("One or more specified Apprise URL(s) are invalid")
|
||||
return None
|
||||
|
||||
return AppriseNotificationService(a_obj)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Any, TypeVar, cast
|
||||
from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy
|
||||
from aiohttp import ClientSession
|
||||
from pyasuswrt import AsusWrtError, AsusWrtHttp
|
||||
from pyasuswrt.exceptions import AsusWrtNotAvailableInfoError
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@@ -354,13 +355,14 @@ class AsusWrtHttpBridge(AsusWrtBridge):
|
||||
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
|
||||
"""Return a dictionary of available sensors for this bridge."""
|
||||
sensors_temperatures = await self._get_available_temperature_sensors()
|
||||
sensors_loadavg = await self._get_loadavg_sensors_availability()
|
||||
sensors_types = {
|
||||
SENSORS_TYPE_BYTES: {
|
||||
KEY_SENSORS: SENSORS_BYTES,
|
||||
KEY_METHOD: self._get_bytes,
|
||||
},
|
||||
SENSORS_TYPE_LOAD_AVG: {
|
||||
KEY_SENSORS: SENSORS_LOAD_AVG,
|
||||
KEY_SENSORS: sensors_loadavg,
|
||||
KEY_METHOD: self._get_load_avg,
|
||||
},
|
||||
SENSORS_TYPE_RATES: {
|
||||
@@ -393,6 +395,16 @@ class AsusWrtHttpBridge(AsusWrtBridge):
|
||||
return []
|
||||
return available_sensors
|
||||
|
||||
async def _get_loadavg_sensors_availability(self) -> list[str]:
|
||||
"""Check if load avg is available on the router."""
|
||||
try:
|
||||
await self._api.async_get_loadavg()
|
||||
except AsusWrtNotAvailableInfoError:
|
||||
return []
|
||||
except AsusWrtError:
|
||||
pass
|
||||
return SENSORS_LOAD_AVG
|
||||
|
||||
@handle_errors_and_zip(AsusWrtError, SENSORS_BYTES)
|
||||
async def _get_bytes(self) -> Any:
|
||||
"""Fetch byte information from the router."""
|
||||
|
||||
@@ -46,6 +46,7 @@ class AtagThermostat(AtagEntity, ClimateEntity):
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
||||
)
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, coordinator, atag_id):
|
||||
"""Initialize an Atag climate device."""
|
||||
|
||||
@@ -249,10 +249,11 @@ class AugustData(AugustSubscriberMixin):
|
||||
device = self.get_device_detail(device_id)
|
||||
activities = activities_from_pubnub_message(device, date_time, message)
|
||||
activity_stream = self.activity_stream
|
||||
if activities:
|
||||
activity_stream.async_process_newer_device_activities(activities)
|
||||
if activities and activity_stream.async_process_newer_device_activities(
|
||||
activities
|
||||
):
|
||||
self.async_signal_device_id_update(device.device_id)
|
||||
activity_stream.async_schedule_house_id_refresh(device.house_id)
|
||||
activity_stream.async_schedule_house_id_refresh(device.house_id)
|
||||
|
||||
@callback
|
||||
def async_stop(self) -> None:
|
||||
|
||||
@@ -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.4.0"]
|
||||
"requirements": ["yalexs==1.11.2", "yalexs-ble==2.4.1"]
|
||||
}
|
||||
|
||||
@@ -33,10 +33,15 @@ async def async_setup_entry(
|
||||
class BAFAutoComfort(BAFEntity, ClimateEntity):
|
||||
"""BAF climate auto comfort."""
|
||||
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY]
|
||||
_attr_translation_key = "auto_comfort"
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
|
||||
@@ -63,6 +63,7 @@ class BalboaClimateEntity(BalboaEntity, ClimateEntity):
|
||||
)
|
||||
_attr_translation_key = DOMAIN
|
||||
_attr_name = None
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, client: SpaClient) -> None:
|
||||
"""Initialize the climate entity."""
|
||||
|
||||
@@ -53,8 +53,13 @@ async def async_setup_entry(
|
||||
class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEntity):
|
||||
"""Representation of a BleBox climate feature (saunaBox)."""
|
||||
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BringDataUpdateCoordinator
|
||||
@@ -29,14 +30,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
email = entry.data[CONF_EMAIL]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
|
||||
bring = Bring(email, password)
|
||||
|
||||
def login_and_load_lists() -> None:
|
||||
bring.login()
|
||||
bring.loadLists()
|
||||
session = async_get_clientsession(hass)
|
||||
bring = Bring(email, password, sessionAsync=session)
|
||||
|
||||
try:
|
||||
await hass.async_add_executor_job(login_and_load_lists)
|
||||
await bring.loginAsync()
|
||||
await bring.loadListsAsync()
|
||||
except BringRequestException as e:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Timeout while connecting for email '{email}'"
|
||||
|
||||
@@ -11,6 +11,7 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
@@ -48,14 +49,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
bring = Bring(user_input[CONF_EMAIL], user_input[CONF_PASSWORD])
|
||||
|
||||
def login_and_load_lists() -> None:
|
||||
bring.login()
|
||||
bring.loadLists()
|
||||
session = async_get_clientsession(self.hass)
|
||||
bring = Bring(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD], sessionAsync=session
|
||||
)
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(login_and_load_lists)
|
||||
await bring.loginAsync()
|
||||
await bring.loadListsAsync()
|
||||
except BringRequestException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except BringAuthException:
|
||||
|
||||
@@ -40,9 +40,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, BringData]:
|
||||
try:
|
||||
lists_response = await self.hass.async_add_executor_job(
|
||||
self.bring.loadLists
|
||||
)
|
||||
lists_response = await self.bring.loadListsAsync()
|
||||
except BringRequestException as e:
|
||||
raise UpdateFailed("Unable to connect and retrieve data from bring") from e
|
||||
except BringParseException as e:
|
||||
@@ -51,9 +49,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
||||
list_dict = {}
|
||||
for lst in lists_response["lists"]:
|
||||
try:
|
||||
items = await self.hass.async_add_executor_job(
|
||||
self.bring.getItems, lst["listUuid"]
|
||||
)
|
||||
items = await self.bring.getItemsAsync(lst["listUuid"])
|
||||
except BringRequestException as e:
|
||||
raise UpdateFailed(
|
||||
"Unable to connect and retrieve data from bring"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bring",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["python-bring-api==2.0.0"]
|
||||
"requirements": ["python-bring-api==3.0.0"]
|
||||
}
|
||||
|
||||
@@ -91,11 +91,8 @@ class BringTodoListEntity(
|
||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||
"""Add an item to the To-do list."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.saveItem,
|
||||
self.bring_list["listUuid"],
|
||||
item.summary,
|
||||
item.description or "",
|
||||
await self.coordinator.bring.saveItemAsync(
|
||||
self.bring_list["listUuid"], item.summary, item.description or ""
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to save todo item for bring") from e
|
||||
@@ -126,16 +123,14 @@ class BringTodoListEntity(
|
||||
assert item.uid
|
||||
|
||||
if item.status == TodoItemStatus.COMPLETED:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.removeItem,
|
||||
await self.coordinator.bring.removeItemAsync(
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
)
|
||||
|
||||
elif item.summary == item.uid:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.updateItem,
|
||||
await self.coordinator.bring.updateItemAsync(
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
item.description or "",
|
||||
@@ -144,13 +139,11 @@ class BringTodoListEntity(
|
||||
raise HomeAssistantError("Unable to update todo item for bring") from e
|
||||
else:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.removeItem,
|
||||
await self.coordinator.bring.removeItemAsync(
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.saveItem,
|
||||
await self.coordinator.bring.saveItemAsync(
|
||||
bring_list["listUuid"],
|
||||
item.summary,
|
||||
item.description or "",
|
||||
@@ -164,8 +157,8 @@ class BringTodoListEntity(
|
||||
"""Delete an item from the To-do list."""
|
||||
for uid in uids:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.removeItem, self.bring_list["listUuid"], uid
|
||||
await self.coordinator.bring.removeItemAsync(
|
||||
self.bring_list["listUuid"], uid
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to delete todo item for bring") from e
|
||||
|
||||
@@ -35,9 +35,14 @@ class BroadlinkThermostat(ClimateEntity, BroadlinkEntity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF, HVACMode.AUTO]
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_target_temperature_step = PRECISION_HALVES
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, device: BroadlinkDevice) -> None:
|
||||
"""Initialize the climate entity."""
|
||||
|
||||
@@ -73,12 +73,16 @@ class BSBLANClimate(
|
||||
_attr_name = None
|
||||
# Determine preset modes
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_preset_modes = PRESET_MODES
|
||||
|
||||
# Determine hvac modes
|
||||
_attr_hvac_modes = HVAC_MODES
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -98,6 +98,6 @@ class TurboJPEGSingleton:
|
||||
TurboJPEGSingleton.__instance = TurboJPEG()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception(
|
||||
"Error loading libturbojpeg; Cameras may impact HomeKit performance"
|
||||
"Error loading libturbojpeg; Camera snapshot performance will be sub-optimal"
|
||||
)
|
||||
TurboJPEGSingleton.__instance = False
|
||||
|
||||
@@ -64,8 +64,11 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.SWING_MODE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_name = None
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self, ac_host: str, ac_index: int, coordinator: CCM15Coordinator
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Intents for the client integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -36,24 +37,34 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
if not entities:
|
||||
raise intent.IntentHandleError("No climate entities")
|
||||
|
||||
if "area" in slots:
|
||||
# Filter by area
|
||||
area_name = slots["area"]["value"]
|
||||
name_slot = slots.get("name", {})
|
||||
entity_name: str | None = name_slot.get("value")
|
||||
entity_text: str | None = name_slot.get("text")
|
||||
|
||||
area_slot = slots.get("area", {})
|
||||
area_id = area_slot.get("value")
|
||||
|
||||
if area_id:
|
||||
# Filter by area and optionally name
|
||||
area_name = area_slot.get("text")
|
||||
|
||||
for maybe_climate in intent.async_match_states(
|
||||
hass, area_name=area_name, domains=[DOMAIN]
|
||||
hass, name=entity_name, area_name=area_id, domains=[DOMAIN]
|
||||
):
|
||||
climate_state = maybe_climate
|
||||
break
|
||||
|
||||
if climate_state is None:
|
||||
raise intent.IntentHandleError(f"No climate entity in area {area_name}")
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_text or entity_name,
|
||||
area=area_name or area_id,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
climate_entity = component.get_entity(climate_state.entity_id)
|
||||
elif "name" in slots:
|
||||
elif entity_name:
|
||||
# Filter by name
|
||||
entity_name = slots["name"]["value"]
|
||||
|
||||
for maybe_climate in intent.async_match_states(
|
||||
hass, name=entity_name, domains=[DOMAIN]
|
||||
):
|
||||
@@ -61,7 +72,12 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
break
|
||||
|
||||
if climate_state is None:
|
||||
raise intent.IntentHandleError(f"No climate entity named {entity_name}")
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_name,
|
||||
area=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
climate_entity = component.get_entity(climate_state.entity_id)
|
||||
else:
|
||||
|
||||
@@ -68,13 +68,22 @@ async def _async_reproduce_states(
|
||||
[ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW],
|
||||
)
|
||||
|
||||
if ATTR_PRESET_MODE in state.attributes:
|
||||
if (
|
||||
ATTR_PRESET_MODE in state.attributes
|
||||
and state.attributes[ATTR_PRESET_MODE] is not None
|
||||
):
|
||||
await call_service(SERVICE_SET_PRESET_MODE, [ATTR_PRESET_MODE])
|
||||
|
||||
if ATTR_SWING_MODE in state.attributes:
|
||||
if (
|
||||
ATTR_SWING_MODE in state.attributes
|
||||
and state.attributes[ATTR_SWING_MODE] is not None
|
||||
):
|
||||
await call_service(SERVICE_SET_SWING_MODE, [ATTR_SWING_MODE])
|
||||
|
||||
if ATTR_FAN_MODE in state.attributes:
|
||||
if (
|
||||
ATTR_FAN_MODE in state.attributes
|
||||
and state.attributes[ATTR_FAN_MODE] is not None
|
||||
):
|
||||
await call_service(SERVICE_SET_FAN_MODE, [ATTR_FAN_MODE])
|
||||
|
||||
if ATTR_HUMIDITY in state.attributes:
|
||||
|
||||
@@ -4,8 +4,12 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from aioelectricitymaps import ElectricityMaps
|
||||
from aioelectricitymaps.exceptions import ElectricityMapsError, InvalidToken
|
||||
from aioelectricitymaps import (
|
||||
ElectricityMaps,
|
||||
ElectricityMapsError,
|
||||
ElectricityMapsInvalidTokenError,
|
||||
ElectricityMapsNoDataError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -146,8 +150,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
try:
|
||||
await fetch_latest_carbon_intensity(self.hass, em, data)
|
||||
except InvalidToken:
|
||||
except ElectricityMapsInvalidTokenError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except ElectricityMapsNoDataError:
|
||||
errors["base"] = "no_data"
|
||||
except ElectricityMapsError:
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
|
||||
@@ -4,9 +4,12 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aioelectricitymaps import ElectricityMaps
|
||||
from aioelectricitymaps.exceptions import ElectricityMapsError, InvalidToken
|
||||
from aioelectricitymaps.models import CarbonIntensityResponse
|
||||
from aioelectricitymaps import (
|
||||
CarbonIntensityResponse,
|
||||
ElectricityMaps,
|
||||
ElectricityMapsError,
|
||||
ElectricityMapsInvalidTokenError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -43,7 +46,7 @@ class CO2SignalCoordinator(DataUpdateCoordinator[CarbonIntensityResponse]):
|
||||
return await fetch_latest_carbon_intensity(
|
||||
self.hass, self.client, self.config_entry.data
|
||||
)
|
||||
except InvalidToken as err:
|
||||
except ElectricityMapsInvalidTokenError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except ElectricityMapsError as err:
|
||||
raise UpdateFailed(str(err)) from err
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioelectricitymaps"],
|
||||
"requirements": ["aioelectricitymaps==0.2.0"]
|
||||
"requirements": ["aioelectricitymaps==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -28,12 +28,9 @@
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"api_ratelimit": "API Ratelimit exceeded"
|
||||
"no_data": "No data is available for the location you have selected."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"api_ratelimit": "[%key:component::co2signal::config::error::api_ratelimit%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Support for climates."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import CLIMATE, SLEEP_BETWEEN_CALLS
|
||||
from aiocomelit.const import CLIMATE
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
@@ -91,11 +90,16 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
|
||||
_attr_hvac_modes = [HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF]
|
||||
_attr_max_temp = 30
|
||||
_attr_min_temp = 5
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_target_temperature_step = PRECISION_TENTHS
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -186,7 +190,6 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, ClimaAction.MANUAL
|
||||
)
|
||||
await asyncio.sleep(SLEEP_BETWEEN_CALLS)
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, ClimaAction.SET, target_temp
|
||||
)
|
||||
@@ -198,7 +201,6 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, ClimaAction.ON
|
||||
)
|
||||
await asyncio.sleep(SLEEP_BETWEEN_CALLS)
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, MODE_TO_ACTION[hvac_mode]
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/comelit",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"requirements": ["aiocomelit==0.8.2"]
|
||||
"requirements": ["aiocomelit==0.8.3"]
|
||||
}
|
||||
|
||||
@@ -349,7 +349,7 @@ async def websocket_hass_agent_debug(
|
||||
},
|
||||
# Slot values that would be received by the intent
|
||||
"slots": { # direct access to values
|
||||
entity_key: entity.value
|
||||
entity_key: entity.text or entity.value
|
||||
for entity_key, entity in result.entities.items()
|
||||
},
|
||||
# Extra slot details, such as the originally matched text
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Standard conversation implementation for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@@ -12,22 +13,15 @@ import re
|
||||
from typing import IO, Any
|
||||
|
||||
from hassil.expression import Expression, ListReference, Sequence
|
||||
from hassil.intents import (
|
||||
Intents,
|
||||
ResponseType,
|
||||
SlotList,
|
||||
TextSlotList,
|
||||
WildcardSlotList,
|
||||
)
|
||||
from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList
|
||||
from hassil.recognize import (
|
||||
MISSING_ENTITY,
|
||||
RecognizeResult,
|
||||
UnmatchedEntity,
|
||||
UnmatchedTextEntity,
|
||||
recognize_all,
|
||||
)
|
||||
from hassil.util import merge_dict
|
||||
from home_assistant_intents import get_intents, get_languages
|
||||
from home_assistant_intents import ErrorKey, get_intents, get_languages
|
||||
import yaml
|
||||
|
||||
from homeassistant import core, setup
|
||||
@@ -229,24 +223,27 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
# Check if a trigger matched
|
||||
if isinstance(result, SentenceTriggerResult):
|
||||
# Gather callback responses in parallel
|
||||
trigger_responses = await asyncio.gather(
|
||||
*(
|
||||
self._trigger_sentences[trigger_id].callback(
|
||||
result.sentence, trigger_result
|
||||
)
|
||||
for trigger_id, trigger_result in result.matched_triggers.items()
|
||||
trigger_callbacks = [
|
||||
self._trigger_sentences[trigger_id].callback(
|
||||
result.sentence, trigger_result
|
||||
)
|
||||
)
|
||||
for trigger_id, trigger_result in result.matched_triggers.items()
|
||||
]
|
||||
|
||||
# Use last non-empty result as response
|
||||
# Use last non-empty result as response.
|
||||
#
|
||||
# There may be multiple copies of a trigger running when editing in
|
||||
# the UI, so it's critical that we filter out empty responses here.
|
||||
response_text: str | None = None
|
||||
for trigger_response in trigger_responses:
|
||||
response_text = response_text or trigger_response
|
||||
for trigger_future in asyncio.as_completed(trigger_callbacks):
|
||||
if trigger_response := await trigger_future:
|
||||
response_text = trigger_response
|
||||
break
|
||||
|
||||
# Convert to conversation result
|
||||
response = intent.IntentResponse(language=language)
|
||||
response.response_type = intent.IntentResponseType.ACTION_DONE
|
||||
response.async_set_speech(response_text or "")
|
||||
response.async_set_speech(response_text or "Done")
|
||||
|
||||
return ConversationResult(response=response)
|
||||
|
||||
@@ -259,7 +256,7 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
return _make_error_result(
|
||||
language,
|
||||
intent.IntentResponseErrorCode.NO_INTENT_MATCH,
|
||||
self._get_error_text(ResponseType.NO_INTENT, lang_intents),
|
||||
self._get_error_text(ErrorKey.NO_INTENT, lang_intents),
|
||||
conversation_id,
|
||||
)
|
||||
|
||||
@@ -268,14 +265,14 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
_LOGGER.debug(
|
||||
"Recognized intent '%s' for template '%s' but had unmatched: %s",
|
||||
result.intent.name,
|
||||
result.intent_sentence.text
|
||||
if result.intent_sentence is not None
|
||||
else "",
|
||||
(
|
||||
result.intent_sentence.text
|
||||
if result.intent_sentence is not None
|
||||
else ""
|
||||
),
|
||||
result.unmatched_entities_list,
|
||||
)
|
||||
error_response_type, error_response_args = _get_unmatched_response(
|
||||
result.unmatched_entities
|
||||
)
|
||||
error_response_type, error_response_args = _get_unmatched_response(result)
|
||||
return _make_error_result(
|
||||
language,
|
||||
intent.IntentResponseErrorCode.NO_VALID_TARGETS,
|
||||
@@ -291,7 +288,8 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
# Slot values to pass to the intent
|
||||
slots = {
|
||||
entity.name: {"value": entity.value} for entity in result.entities_list
|
||||
entity.name: {"value": entity.value, "text": entity.text or entity.value}
|
||||
for entity in result.entities_list
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -318,6 +316,20 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
),
|
||||
conversation_id,
|
||||
)
|
||||
except intent.DuplicateNamesMatchedError as duplicate_names_error:
|
||||
# Intent was valid, but two or more entities with the same name matched.
|
||||
(
|
||||
error_response_type,
|
||||
error_response_args,
|
||||
) = _get_duplicate_names_matched_response(duplicate_names_error)
|
||||
return _make_error_result(
|
||||
language,
|
||||
intent.IntentResponseErrorCode.NO_VALID_TARGETS,
|
||||
self._get_error_text(
|
||||
error_response_type, lang_intents, **error_response_args
|
||||
),
|
||||
conversation_id,
|
||||
)
|
||||
except intent.IntentHandleError:
|
||||
# Intent was valid and entities matched constraints, but an error
|
||||
# occurred during handling.
|
||||
@@ -325,7 +337,7 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
return _make_error_result(
|
||||
language,
|
||||
intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
|
||||
self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents),
|
||||
self._get_error_text(ErrorKey.HANDLE_ERROR, lang_intents),
|
||||
conversation_id,
|
||||
)
|
||||
except intent.IntentUnexpectedError:
|
||||
@@ -333,7 +345,7 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
return _make_error_result(
|
||||
language,
|
||||
intent.IntentResponseErrorCode.UNKNOWN,
|
||||
self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents),
|
||||
self._get_error_text(ErrorKey.HANDLE_ERROR, lang_intents),
|
||||
conversation_id,
|
||||
)
|
||||
|
||||
@@ -480,9 +492,11 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
for entity_name, entity_value in recognize_result.entities.items()
|
||||
},
|
||||
# First matched or unmatched state
|
||||
"state": template.TemplateState(self.hass, state1)
|
||||
if state1 is not None
|
||||
else None,
|
||||
"state": (
|
||||
template.TemplateState(self.hass, state1)
|
||||
if state1 is not None
|
||||
else None
|
||||
),
|
||||
"query": {
|
||||
# Entity states that matched the query (e.g, "on")
|
||||
"matched": [
|
||||
@@ -724,7 +738,12 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
if async_should_expose(self.hass, DOMAIN, state.entity_id)
|
||||
]
|
||||
|
||||
# Gather exposed entity names
|
||||
# Gather exposed entity names.
|
||||
#
|
||||
# NOTE: We do not pass entity ids in here because multiple entities may
|
||||
# have the same name. The intent matcher doesn't gather all matching
|
||||
# values for a list, just the first. So we will need to match by name no
|
||||
# matter what.
|
||||
entity_names = []
|
||||
for state in states:
|
||||
# Checked against "requires_context" and "excludes_context" in hassil
|
||||
@@ -753,7 +772,10 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
# Default name
|
||||
entity_names.append((state.name, state.name, context))
|
||||
|
||||
# Expose all areas
|
||||
# Expose all areas.
|
||||
#
|
||||
# We pass in area id here with the expectation that no two areas will
|
||||
# share the same name or alias.
|
||||
areas = ar.async_get(self.hass)
|
||||
area_names = []
|
||||
for area in areas.async_list_areas():
|
||||
@@ -791,11 +813,11 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
if device_area is None:
|
||||
return None
|
||||
|
||||
return {"area": device_area.id}
|
||||
return {"area": {"value": device_area.id, "text": device_area.name}}
|
||||
|
||||
def _get_error_text(
|
||||
self,
|
||||
response_type: ResponseType,
|
||||
error_key: ErrorKey,
|
||||
lang_intents: LanguageIntents | None,
|
||||
**response_args,
|
||||
) -> str:
|
||||
@@ -803,7 +825,7 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
if lang_intents is None:
|
||||
return _DEFAULT_ERROR_TEXT
|
||||
|
||||
response_key = response_type.value
|
||||
response_key = error_key.value
|
||||
response_str = (
|
||||
lang_intents.error_responses.get(response_key) or _DEFAULT_ERROR_TEXT
|
||||
)
|
||||
@@ -916,59 +938,86 @@ def _make_error_result(
|
||||
return ConversationResult(response, conversation_id)
|
||||
|
||||
|
||||
def _get_unmatched_response(
|
||||
unmatched_entities: dict[str, UnmatchedEntity],
|
||||
) -> tuple[ResponseType, dict[str, Any]]:
|
||||
error_response_type = ResponseType.NO_INTENT
|
||||
error_response_args: dict[str, Any] = {}
|
||||
def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str, Any]]:
|
||||
"""Get key and template arguments for error when there are unmatched intent entities/slots."""
|
||||
|
||||
if unmatched_name := unmatched_entities.get("name"):
|
||||
# Unmatched device or entity
|
||||
assert isinstance(unmatched_name, UnmatchedTextEntity)
|
||||
error_response_type = ResponseType.NO_ENTITY
|
||||
error_response_args["entity"] = unmatched_name.text
|
||||
# Filter out non-text and missing context entities
|
||||
unmatched_text: dict[str, str] = {
|
||||
key: entity.text.strip()
|
||||
for key, entity in result.unmatched_entities.items()
|
||||
if isinstance(entity, UnmatchedTextEntity) and entity.text != MISSING_ENTITY
|
||||
}
|
||||
|
||||
elif unmatched_area := unmatched_entities.get("area"):
|
||||
# Unmatched area
|
||||
assert isinstance(unmatched_area, UnmatchedTextEntity)
|
||||
error_response_type = ResponseType.NO_AREA
|
||||
error_response_args["area"] = unmatched_area.text
|
||||
if unmatched_area := unmatched_text.get("area"):
|
||||
# area only
|
||||
return ErrorKey.NO_AREA, {"area": unmatched_area}
|
||||
|
||||
return error_response_type, error_response_args
|
||||
# Area may still have matched
|
||||
matched_area: str | None = None
|
||||
if matched_area_entity := result.entities.get("area"):
|
||||
matched_area = matched_area_entity.text.strip()
|
||||
|
||||
if unmatched_name := unmatched_text.get("name"):
|
||||
if matched_area:
|
||||
# device in area
|
||||
return ErrorKey.NO_ENTITY_IN_AREA, {
|
||||
"entity": unmatched_name,
|
||||
"area": matched_area,
|
||||
}
|
||||
|
||||
# device only
|
||||
return ErrorKey.NO_ENTITY, {"entity": unmatched_name}
|
||||
|
||||
# Default error
|
||||
return ErrorKey.NO_INTENT, {}
|
||||
|
||||
|
||||
def _get_no_states_matched_response(
|
||||
no_states_error: intent.NoStatesMatchedError,
|
||||
) -> tuple[ResponseType, dict[str, Any]]:
|
||||
"""Return error response type and template arguments for error."""
|
||||
if not (
|
||||
no_states_error.area
|
||||
and (no_states_error.device_classes or no_states_error.domains)
|
||||
):
|
||||
# Device class and domain must be paired with an area for the error
|
||||
# message.
|
||||
return ResponseType.NO_INTENT, {}
|
||||
) -> tuple[ErrorKey, dict[str, Any]]:
|
||||
"""Return key and template arguments for error when intent returns no matching states."""
|
||||
|
||||
error_response_args: dict[str, Any] = {"area": no_states_error.area}
|
||||
|
||||
# Check device classes first, since it's more specific than domain
|
||||
# Device classes should be checked before domains
|
||||
if no_states_error.device_classes:
|
||||
# No exposed entities of a particular class in an area.
|
||||
# Example: "close the bedroom windows"
|
||||
#
|
||||
# Only use the first device class for the error message
|
||||
error_response_args["device_class"] = next(iter(no_states_error.device_classes))
|
||||
device_class = next(iter(no_states_error.device_classes)) # first device class
|
||||
if no_states_error.area:
|
||||
# device_class in area
|
||||
return ErrorKey.NO_DEVICE_CLASS_IN_AREA, {
|
||||
"device_class": device_class,
|
||||
"area": no_states_error.area,
|
||||
}
|
||||
|
||||
return ResponseType.NO_DEVICE_CLASS, error_response_args
|
||||
# device_class only
|
||||
return ErrorKey.NO_DEVICE_CLASS, {"device_class": device_class}
|
||||
|
||||
# No exposed entities of a domain in an area.
|
||||
# Example: "turn on lights in kitchen"
|
||||
assert no_states_error.domains
|
||||
#
|
||||
# Only use the first domain for the error message
|
||||
error_response_args["domain"] = next(iter(no_states_error.domains))
|
||||
if no_states_error.domains:
|
||||
domain = next(iter(no_states_error.domains)) # first domain
|
||||
if no_states_error.area:
|
||||
# domain in area
|
||||
return ErrorKey.NO_DOMAIN_IN_AREA, {
|
||||
"domain": domain,
|
||||
"area": no_states_error.area,
|
||||
}
|
||||
|
||||
return ResponseType.NO_DOMAIN, error_response_args
|
||||
# domain only
|
||||
return ErrorKey.NO_DOMAIN, {"domain": domain}
|
||||
|
||||
# Default error
|
||||
return ErrorKey.NO_INTENT, {}
|
||||
|
||||
|
||||
def _get_duplicate_names_matched_response(
|
||||
duplicate_names_error: intent.DuplicateNamesMatchedError,
|
||||
) -> tuple[ErrorKey, dict[str, Any]]:
|
||||
"""Return key and template arguments for error when intent returns duplicate matches."""
|
||||
|
||||
if duplicate_names_error.area:
|
||||
return ErrorKey.DUPLICATE_ENTITIES_IN_AREA, {
|
||||
"entity": duplicate_names_error.name,
|
||||
"area": duplicate_names_error.area,
|
||||
}
|
||||
|
||||
return ErrorKey.DUPLICATE_ENTITIES, {"entity": duplicate_names_error.name}
|
||||
|
||||
|
||||
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.6.0", "home-assistant-intents==2024.1.29"]
|
||||
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.2.2"]
|
||||
}
|
||||
|
||||
@@ -98,7 +98,12 @@ async def async_attach_trigger(
|
||||
# mypy does not understand the type narrowing, unclear why
|
||||
return automation_result.conversation_response # type: ignore[return-value]
|
||||
|
||||
return "Done"
|
||||
# It's important to return None here instead of a string.
|
||||
#
|
||||
# When editing in the UI, a copy of this trigger is registered.
|
||||
# If we return a string from here, there is a race condition between the
|
||||
# two trigger copies for who will provide a response.
|
||||
return None
|
||||
|
||||
default_agent = await _get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT)
|
||||
assert isinstance(default_agent, DefaultAgent)
|
||||
|
||||
@@ -54,6 +54,7 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity):
|
||||
"""Representation of a coolmaster climate device."""
|
||||
|
||||
_attr_name = None
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, coordinator, unit_id, info, supported_modes):
|
||||
"""Initialize the climate device."""
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"services": {
|
||||
"decrement": "mdi:numeric-negative-1",
|
||||
"increment": "mdi:numeric-positive-1",
|
||||
"reset": "mdi:refresh",
|
||||
"set_value": "mdi:counter"
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,7 @@ class DaikinClimate(ClimateEntity):
|
||||
_attr_target_temperature_step = 1
|
||||
_attr_fan_modes: list[str]
|
||||
_attr_swing_modes: list[str]
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, api: DaikinApi) -> None:
|
||||
"""Initialize the climate device."""
|
||||
|
||||
@@ -100,6 +100,7 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity):
|
||||
TYPE = DOMAIN
|
||||
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, device: Thermostat, gateway: DeconzGateway) -> None:
|
||||
"""Set up thermostat device."""
|
||||
@@ -119,7 +120,11 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity):
|
||||
HVAC_MODE_TO_DECONZ[item]: item for item in self._attr_hvac_modes
|
||||
}
|
||||
|
||||
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
self._attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
if device.fan_mode:
|
||||
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["deluge_client"],
|
||||
"requirements": ["deluge-client==1.7.1"]
|
||||
"requirements": ["deluge-client==1.10.2"]
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ class DemoClimate(ClimateEntity):
|
||||
_attr_name = None
|
||||
_attr_should_poll = False
|
||||
_attr_translation_key = "ubercool"
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -137,6 +138,9 @@ class DemoClimate(ClimateEntity):
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
self._target_temperature = target_temperature
|
||||
self._target_humidity = target_humidity
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"derivative": {
|
||||
"default": "mdi:chart-line"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,6 @@ UNIT_TIME = {
|
||||
UnitOfTime.DAYS: 24 * 60 * 60,
|
||||
}
|
||||
|
||||
ICON = "mdi:chart-line"
|
||||
|
||||
DEFAULT_ROUND = 3
|
||||
DEFAULT_TIME_WINDOW = 0
|
||||
|
||||
@@ -157,9 +155,9 @@ async def async_setup_platform(
|
||||
|
||||
|
||||
class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
"""Representation of an derivative sensor."""
|
||||
"""Representation of a derivative sensor."""
|
||||
|
||||
_attr_icon = ICON
|
||||
_attr_translation_key = "derivative"
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -56,6 +56,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit
|
||||
_attr_precision = PRECISION_TENTHS
|
||||
_attr_hvac_mode = HVACMode.HEAT
|
||||
_attr_hvac_modes = [HVACMode.HEAT]
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.38.1", "getmac==0.9.4"],
|
||||
"requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["async-upnp-client==0.38.1"],
|
||||
"requirements": ["async-upnp-client==0.38.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
|
||||
@@ -47,12 +47,16 @@ class DuotecnoClimate(DuotecnoEntity, ClimateEntity):
|
||||
|
||||
_unit: SensUnit
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_hvac_modes = list(HVACMODE_REVERSE)
|
||||
_attr_preset_modes = list(PRESETMODES)
|
||||
_attr_translation_key = "duotecno"
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/easyenergy",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["easyenergy==2.1.0"]
|
||||
"requirements": ["easyenergy==2.1.1"]
|
||||
}
|
||||
|
||||
@@ -323,6 +323,7 @@ class Thermostat(ClimateEntity):
|
||||
_attr_fan_modes = [FAN_AUTO, FAN_ON]
|
||||
_attr_name = None
|
||||
_attr_has_entity_name = True
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self, data: EcobeeData, thermostat_index: int, thermostat: dict
|
||||
@@ -375,6 +376,10 @@ class Thermostat(ClimateEntity):
|
||||
supported = supported | ClimateEntityFeature.TARGET_HUMIDITY
|
||||
if self.has_aux_heat:
|
||||
supported = supported | ClimateEntityFeature.AUX_HEAT
|
||||
if len(self.hvac_modes) > 1 and HVACMode.OFF in self.hvac_modes:
|
||||
supported = (
|
||||
supported | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
return supported
|
||||
|
||||
@property
|
||||
|
||||
@@ -66,6 +66,7 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity):
|
||||
|
||||
_attr_should_poll = True
|
||||
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, thermostat):
|
||||
"""Initialize."""
|
||||
@@ -79,12 +80,13 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity):
|
||||
ha_mode = ECONET_STATE_TO_HA[mode]
|
||||
self._attr_hvac_modes.append(ha_mode)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
"""Return the list of supported features."""
|
||||
if self._econet.supports_humidifier:
|
||||
return SUPPORT_FLAGS_THERMOSTAT | ClimateEntityFeature.TARGET_HUMIDITY
|
||||
return SUPPORT_FLAGS_THERMOSTAT
|
||||
self._attr_supported_features |= SUPPORT_FLAGS_THERMOSTAT
|
||||
if thermostat.supports_humidifier:
|
||||
self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY
|
||||
if len(self.hvac_modes) > 1 and HVACMode.OFF in self.hvac_modes:
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
||||
@@ -75,7 +75,7 @@ async def _validate_input(
|
||||
rest_config = create_rest_config(
|
||||
aiohttp_client.async_get_clientsession(hass),
|
||||
device_id=device_id,
|
||||
country=country,
|
||||
alpha_2_country=country,
|
||||
override_rest_url=rest_url,
|
||||
)
|
||||
|
||||
@@ -266,6 +266,10 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# If not we will inform the user about the mismatch.
|
||||
error = None
|
||||
placeholders = None
|
||||
|
||||
# Convert the country to upper case as ISO 3166-1 alpha-2 country codes are upper case
|
||||
user_input[CONF_COUNTRY] = user_input[CONF_COUNTRY].upper()
|
||||
|
||||
if len(user_input[CONF_COUNTRY]) != 2:
|
||||
error = "invalid_country_length"
|
||||
placeholders = {"countries_url": "https://www.iso.org/obp/ui/#search/code/"}
|
||||
|
||||
@@ -49,7 +49,7 @@ class EcovacsController:
|
||||
create_rest_config(
|
||||
aiohttp_client.async_get_clientsession(self._hass),
|
||||
device_id=self._device_id,
|
||||
country=country,
|
||||
alpha_2_country=country,
|
||||
override_rest_url=config.get(CONF_OVERRIDE_REST_URL),
|
||||
),
|
||||
config[CONF_USERNAME],
|
||||
@@ -74,11 +74,16 @@ class EcovacsController:
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Init controller."""
|
||||
mqtt_config_verfied = False
|
||||
try:
|
||||
devices = await self._api_client.get_devices()
|
||||
credentials = await self._authenticator.authenticate()
|
||||
for device_config in devices:
|
||||
if isinstance(device_config, DeviceInfo):
|
||||
# MQTT device
|
||||
if not mqtt_config_verfied:
|
||||
await self._mqtt.verify_config()
|
||||
mqtt_config_verfied = True
|
||||
device = Device(device_config, self._authenticator)
|
||||
await device.initialize(self._mqtt)
|
||||
self.devices.append(device)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.8", "deebot-client==5.0.0"]
|
||||
"requirements": ["py-sucks==0.9.9", "deebot-client==5.2.2"]
|
||||
}
|
||||
|
||||
@@ -47,13 +47,13 @@
|
||||
"name": "Relocate"
|
||||
},
|
||||
"reset_lifespan_brush": {
|
||||
"name": "Reset brush lifespan"
|
||||
"name": "Reset main brush lifespan"
|
||||
},
|
||||
"reset_lifespan_filter": {
|
||||
"name": "Reset filter lifespan"
|
||||
},
|
||||
"reset_lifespan_side_brush": {
|
||||
"name": "Reset side brush lifespan"
|
||||
"name": "Reset side brushes lifespan"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
@@ -79,13 +79,13 @@
|
||||
}
|
||||
},
|
||||
"lifespan_brush": {
|
||||
"name": "Brush lifespan"
|
||||
"name": "Main brush lifespan"
|
||||
},
|
||||
"lifespan_filter": {
|
||||
"name": "Filter lifespan"
|
||||
},
|
||||
"lifespan_side_brush": {
|
||||
"name": "Side brush lifespan"
|
||||
"name": "Side brushes lifespan"
|
||||
},
|
||||
"network_ip": {
|
||||
"name": "IP address"
|
||||
@@ -100,7 +100,7 @@
|
||||
"name": "Area cleaned"
|
||||
},
|
||||
"stats_time": {
|
||||
"name": "Time cleaned"
|
||||
"name": "Cleaning duration"
|
||||
},
|
||||
"total_stats_area": {
|
||||
"name": "Total area cleaned"
|
||||
@@ -109,12 +109,12 @@
|
||||
"name": "Total cleanings"
|
||||
},
|
||||
"total_stats_time": {
|
||||
"name": "Total time cleaned"
|
||||
"name": "Total cleaning duration"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"water_amount": {
|
||||
"name": "Water amount",
|
||||
"name": "Water flow level",
|
||||
"state": {
|
||||
"high": "High",
|
||||
"low": "Low",
|
||||
@@ -137,7 +137,7 @@
|
||||
"name": "Advanced mode"
|
||||
},
|
||||
"carpet_auto_fan_boost": {
|
||||
"name": "Carpet auto fan speed boost"
|
||||
"name": "Carpet auto-boost suction"
|
||||
},
|
||||
"clean_preference": {
|
||||
"name": "Clean preference"
|
||||
|
||||
@@ -95,7 +95,7 @@ class EcovacsLegacyVacuum(StateVacuumEntity):
|
||||
This will not change the entity's state. If the error caused the state
|
||||
to change, that will come through as a separate on_status event
|
||||
"""
|
||||
if error == "no_error":
|
||||
if error in ["no_error", sucks.ERROR_CODES["100"]]:
|
||||
self.error = None
|
||||
else:
|
||||
self.error = error
|
||||
|
||||
@@ -38,8 +38,8 @@ class EcowittEntity(Entity):
|
||||
"""Update the state on callback."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.ecowitt.update_cb.append(_update_state) # type: ignore[arg-type] # upstream bug
|
||||
self.async_on_remove(lambda: self.ecowitt.update_cb.remove(_update_state)) # type: ignore[arg-type] # upstream bug
|
||||
self.ecowitt.update_cb.append(_update_state)
|
||||
self.async_on_remove(lambda: self.ecowitt.update_cb.remove(_update_state))
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["webhook"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["aioecowitt==2023.5.0"]
|
||||
"requirements": ["aioecowitt==2024.2.1"]
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ class ElectraClimateEntity(ClimateEntity):
|
||||
_attr_hvac_modes = ELECTRA_MODES
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, device: ElectraAirConditioner, api: ElectraAPI) -> None:
|
||||
"""Initialize Electra climate entity."""
|
||||
@@ -121,6 +122,8 @@ class ElectraClimateEntity(ClimateEntity):
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
swing_modes: list = []
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["elgato==5.1.1"],
|
||||
"requirements": ["elgato==5.1.2"],
|
||||
"zeroconf": ["_elg._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from elkm1_lib.elements import Element
|
||||
from elkm1_lib.elk import Elk
|
||||
from elkm1_lib.elk import Elk, Panel
|
||||
from elkm1_lib.util import parse_url
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -398,22 +398,30 @@ async def async_wait_for_elk_to_sync(
|
||||
return success
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_elk_panel(hass: HomeAssistant, service: ServiceCall) -> Panel:
|
||||
"""Get the ElkM1 panel from a service call."""
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk.panel
|
||||
|
||||
|
||||
def _create_elk_services(hass: HomeAssistant) -> None:
|
||||
def _getelk(service: ServiceCall) -> Elk:
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk
|
||||
"""Create ElkM1 services."""
|
||||
|
||||
@callback
|
||||
def _speak_word_service(service: ServiceCall) -> None:
|
||||
_getelk(service).panel.speak_word(service.data["number"])
|
||||
_async_get_elk_panel(hass, service).speak_word(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _speak_phrase_service(service: ServiceCall) -> None:
|
||||
_getelk(service).panel.speak_phrase(service.data["number"])
|
||||
_async_get_elk_panel(hass, service).speak_phrase(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _set_time_service(service: ServiceCall) -> None:
|
||||
_getelk(service).panel.set_time(dt_util.now())
|
||||
_async_get_elk_panel(hass, service).set_time(dt_util.now())
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
|
||||
|
||||
@@ -79,6 +79,8 @@ class ElkThermostat(ElkEntity, ClimateEntity):
|
||||
ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.AUX_HEAT
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_min_temp = 1
|
||||
_attr_max_temp = 99
|
||||
@@ -87,6 +89,7 @@ class ElkThermostat(ElkEntity, ClimateEntity):
|
||||
_attr_target_temperature_step = 1
|
||||
_attr_fan_modes = [FAN_AUTO, FAN_ON]
|
||||
_element: Thermostat
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
|
||||
@@ -35,8 +35,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._api_token = api_token = user_input[CONF_API_TOKEN]
|
||||
client = Elvia(meter_value_token=api_token).meter_value()
|
||||
try:
|
||||
end_time = dt_util.utcnow()
|
||||
results = await client.get_meter_values(
|
||||
start_time=(dt_util.now() - timedelta(hours=1)).isoformat()
|
||||
start_time=(end_time - timedelta(hours=1)).isoformat(),
|
||||
end_time=end_time.isoformat(),
|
||||
)
|
||||
|
||||
except ElviaError.AuthError as exception:
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from elvia import Elvia
|
||||
from elvia import Elvia, error as ElviaError
|
||||
|
||||
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
@@ -38,11 +38,18 @@ class ElviaImporter:
|
||||
self.client = Elvia(meter_value_token=api_token).meter_value()
|
||||
self.metering_point_id = metering_point_id
|
||||
|
||||
async def _fetch_hourly_data(self, since: datetime) -> list[MeterValueTimeSeries]:
|
||||
async def _fetch_hourly_data(
|
||||
self,
|
||||
since: datetime,
|
||||
until: datetime,
|
||||
) -> list[MeterValueTimeSeries]:
|
||||
"""Fetch hourly data."""
|
||||
LOGGER.debug("Fetching hourly data since %s", since)
|
||||
start_time = since.isoformat()
|
||||
end_time = until.isoformat()
|
||||
LOGGER.debug("Fetching hourly data %s - %s", start_time, end_time)
|
||||
all_data = await self.client.get_meter_values(
|
||||
start_time=since.isoformat(),
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
metering_point_ids=[self.metering_point_id],
|
||||
)
|
||||
return all_data["meteringpoints"][0]["metervalue"]["timeSeries"]
|
||||
@@ -61,18 +68,37 @@ class ElviaImporter:
|
||||
)
|
||||
|
||||
if not last_stats:
|
||||
# First time we insert 1 years of data (if available)
|
||||
hourly_data = await self._fetch_hourly_data(
|
||||
since=dt_util.now() - timedelta(days=365)
|
||||
)
|
||||
# First time we insert 3 years of data (if available)
|
||||
hourly_data: list[MeterValueTimeSeries] = []
|
||||
until = dt_util.utcnow()
|
||||
for year in (3, 2, 1):
|
||||
try:
|
||||
year_hours = await self._fetch_hourly_data(
|
||||
since=until - timedelta(days=365 * year),
|
||||
until=until - timedelta(days=365 * (year - 1)),
|
||||
)
|
||||
except ElviaError.ElviaException:
|
||||
# This will raise if the contract have no data for the
|
||||
# year, we can safely ignore this
|
||||
continue
|
||||
hourly_data.extend(year_hours)
|
||||
|
||||
if hourly_data is None or len(hourly_data) == 0:
|
||||
LOGGER.error("No data available for the metering point")
|
||||
return
|
||||
last_stats_time = None
|
||||
_sum = 0.0
|
||||
else:
|
||||
hourly_data = await self._fetch_hourly_data(
|
||||
since=dt_util.utc_from_timestamp(last_stats[statistic_id][0]["end"])
|
||||
)
|
||||
try:
|
||||
hourly_data = await self._fetch_hourly_data(
|
||||
since=dt_util.utc_from_timestamp(
|
||||
last_stats[statistic_id][0]["end"]
|
||||
),
|
||||
until=dt_util.utcnow(),
|
||||
)
|
||||
except ElviaError.ElviaException as err:
|
||||
LOGGER.error("Error fetching data: %s", err)
|
||||
return
|
||||
|
||||
if (
|
||||
hourly_data is None
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""Support for Enigma2 media players."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
import contextlib
|
||||
from logging import getLogger
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError, ServerDisconnectedError
|
||||
from openwebif.api import OpenWebIfDevice
|
||||
from openwebif.enums import RemoteControlCodes, SetVolumeOption
|
||||
from openwebif.enums import PowerState, RemoteControlCodes, SetVolumeOption
|
||||
import voluptuous as vol
|
||||
from yarl import URL
|
||||
|
||||
@@ -50,6 +53,8 @@ ATTR_MEDIA_DESCRIPTION = "media_description"
|
||||
ATTR_MEDIA_END_TIME = "media_end_time"
|
||||
ATTR_MEDIA_START_TIME = "media_start_time"
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
@@ -143,7 +148,12 @@ class Enigma2Device(MediaPlayerEntity):
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off media player."""
|
||||
await self._device.turn_off()
|
||||
if self._device.turn_off_to_deep:
|
||||
with contextlib.suppress(ServerDisconnectedError):
|
||||
await self._device.set_powerstate(PowerState.DEEP_STANDBY)
|
||||
self._attr_available = False
|
||||
else:
|
||||
await self._device.set_powerstate(PowerState.STANDBY)
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the media player on."""
|
||||
@@ -191,8 +201,19 @@ class Enigma2Device(MediaPlayerEntity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update state of the media_player."""
|
||||
await self._device.update()
|
||||
self._attr_available = not self._device.is_offline
|
||||
try:
|
||||
await self._device.update()
|
||||
except ClientConnectorError as err:
|
||||
if self._attr_available:
|
||||
_LOGGER.warning(
|
||||
"%s is unavailable. Error: %s", self._device.base.host, err
|
||||
)
|
||||
self._attr_available = False
|
||||
return
|
||||
|
||||
if not self._attr_available:
|
||||
_LOGGER.debug("%s is available", self._device.base.host)
|
||||
self._attr_available = True
|
||||
|
||||
if not self._device.status.in_standby:
|
||||
self._attr_extra_state_attributes = {
|
||||
|
||||
@@ -83,6 +83,7 @@ class EphEmberThermostat(ClimateEntity):
|
||||
|
||||
_attr_hvac_modes = OPERATION_LIST
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, ember, zone):
|
||||
"""Initialize the thermostat."""
|
||||
@@ -100,6 +101,9 @@ class EphEmberThermostat(ClimateEntity):
|
||||
if self._hot_water:
|
||||
self._attr_supported_features = ClimateEntityFeature.AUX_HEAT
|
||||
self._attr_target_temperature_step = None
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
||||
@@ -88,9 +88,9 @@ class EpionSensor(CoordinatorEntity[EpionCoordinator], SensorEntity):
|
||||
super().__init__(coordinator)
|
||||
self._epion_device_id = epion_device_id
|
||||
self.entity_description = description
|
||||
self.unique_id = f"{epion_device_id}_{description.key}"
|
||||
self._attr_unique_id = f"{epion_device_id}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._epion_device_id)},
|
||||
identifiers={(DOMAIN, epion_device_id)},
|
||||
manufacturer="Epion",
|
||||
name=self.device.get("deviceName"),
|
||||
sw_version=self.device.get("fwVersion"),
|
||||
|
||||
@@ -82,10 +82,14 @@ class ControllerEntity(ClimateEntity):
|
||||
_attr_precision = PRECISION_WHOLE
|
||||
_attr_should_poll = False
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, controller: Controller) -> None:
|
||||
"""Initialise ControllerDevice."""
|
||||
|
||||
@@ -137,6 +137,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_translation_key = "climate"
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
@callback
|
||||
def _on_static_info_update(self, static_info: EntityInfo) -> None:
|
||||
@@ -179,6 +180,8 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
features |= ClimateEntityFeature.FAN_MODE
|
||||
if self.swing_modes:
|
||||
features |= ClimateEntityFeature.SWING_MODE
|
||||
if len(self.hvac_modes) > 1 and HVACMode.OFF in self.hvac_modes:
|
||||
features |= ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
self._attr_supported_features = features
|
||||
|
||||
def _get_precision(self) -> float:
|
||||
|
||||
@@ -352,7 +352,6 @@ class ESPHomeManager:
|
||||
if self.voice_assistant_udp_server is not None:
|
||||
_LOGGER.warning("Voice assistant UDP server was not stopped")
|
||||
self.voice_assistant_udp_server.stop()
|
||||
self.voice_assistant_udp_server.close()
|
||||
self.voice_assistant_udp_server = None
|
||||
|
||||
hass = self.hass
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==21.0.1",
|
||||
"aioesphomeapi==21.0.2",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==0.4.1"
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""ESPHome voice assistant support."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@@ -67,7 +68,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
"""Receive UDP packets and forward them to the voice assistant."""
|
||||
|
||||
started = False
|
||||
stopped = False
|
||||
stop_requested = False
|
||||
transport: asyncio.DatagramTransport | None = None
|
||||
remote_addr: tuple[str, int] | None = None
|
||||
|
||||
@@ -92,6 +93,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
self._tts_done = asyncio.Event()
|
||||
self._tts_task: asyncio.Task | None = None
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""True if the the UDP server is started and hasn't been asked to stop."""
|
||||
return self.started and (not self.stop_requested)
|
||||
|
||||
async def start_server(self) -> int:
|
||||
"""Start accepting connections."""
|
||||
|
||||
@@ -99,7 +105,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
"""Accept connection."""
|
||||
if self.started:
|
||||
raise RuntimeError("Can only start once")
|
||||
if self.stopped:
|
||||
if self.stop_requested:
|
||||
raise RuntimeError("No longer accepting connections")
|
||||
|
||||
self.started = True
|
||||
@@ -124,7 +130,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
@callback
|
||||
def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None:
|
||||
"""Handle incoming UDP packet."""
|
||||
if not self.started or self.stopped:
|
||||
if not self.is_running:
|
||||
return
|
||||
if self.remote_addr is None:
|
||||
self.remote_addr = addr
|
||||
@@ -142,19 +148,19 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
def stop(self) -> None:
|
||||
"""Stop the receiver."""
|
||||
self.queue.put_nowait(b"")
|
||||
self.started = False
|
||||
self.stopped = True
|
||||
self.close()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the receiver."""
|
||||
self.started = False
|
||||
self.stopped = True
|
||||
self.stop_requested = True
|
||||
|
||||
if self.transport is not None:
|
||||
self.transport.close()
|
||||
|
||||
async def _iterate_packets(self) -> AsyncIterable[bytes]:
|
||||
"""Iterate over incoming packets."""
|
||||
if not self.started or self.stopped:
|
||||
if not self.is_running:
|
||||
raise RuntimeError("Not running")
|
||||
|
||||
while data := await self.queue.get():
|
||||
@@ -303,8 +309,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
|
||||
async def _send_tts(self, media_id: str) -> None:
|
||||
"""Send TTS audio to device via UDP."""
|
||||
# Always send stream start/end events
|
||||
self.handle_event(VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {})
|
||||
|
||||
try:
|
||||
if self.transport is None:
|
||||
if (not self.is_running) or (self.transport is None):
|
||||
return
|
||||
|
||||
extension, data = await tts.async_get_media_source_audio(
|
||||
@@ -337,15 +346,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
|
||||
_LOGGER.debug("Sending %d bytes of audio", audio_bytes_size)
|
||||
|
||||
self.handle_event(
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {}
|
||||
)
|
||||
|
||||
bytes_per_sample = stt.AudioBitRates.BITRATE_16 // 8
|
||||
sample_offset = 0
|
||||
samples_left = audio_bytes_size // bytes_per_sample
|
||||
|
||||
while samples_left > 0:
|
||||
while (samples_left > 0) and self.is_running:
|
||||
bytes_offset = sample_offset * bytes_per_sample
|
||||
chunk: bytes = audio_bytes[bytes_offset : bytes_offset + 1024]
|
||||
samples_in_chunk = len(chunk) // bytes_per_sample
|
||||
|
||||
@@ -156,6 +156,7 @@ class EvoClimateEntity(EvoDevice, ClimateEntity):
|
||||
"""Base for an evohome Climate device."""
|
||||
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
@@ -190,7 +191,10 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
]
|
||||
|
||||
self._attr_supported_features = (
|
||||
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None:
|
||||
@@ -372,6 +376,9 @@ class EvoController(EvoClimateEntity):
|
||||
]
|
||||
if self._attr_preset_modes:
|
||||
self._attr_supported_features = ClimateEntityFeature.PRESET_MODE
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None:
|
||||
"""Process a service request (system mode) for a controller.
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/evohome",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohomeasync", "evohomeasync2"],
|
||||
"requirements": ["evohome-async==0.4.17"]
|
||||
"requirements": ["evohome-async==0.4.19"]
|
||||
}
|
||||
|
||||
@@ -126,6 +126,8 @@ async def async_setup_entry(
|
||||
class FibaroThermostat(FibaroDevice, ClimateEntity):
|
||||
"""Representation of a Fibaro Thermostat."""
|
||||
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, fibaro_device: DeviceModel) -> None:
|
||||
"""Initialize the Fibaro device."""
|
||||
super().__init__(fibaro_device)
|
||||
@@ -209,6 +211,11 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
|
||||
if mode in OPMODES_PRESET:
|
||||
self._attr_preset_modes.append(OPMODES_PRESET[mode])
|
||||
|
||||
if HVACMode.OFF in self._attr_hvac_modes and len(self._attr_hvac_modes) > 1:
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -69,6 +69,7 @@ class Flexit(ClimateEntity):
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self, hub: ModbusHub, modbus_slave: int | None, name: str | None
|
||||
|
||||
@@ -62,13 +62,17 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
]
|
||||
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
_attr_target_temperature_step = PRECISION_HALVES
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_max_temp = MAX_TEMP
|
||||
_attr_min_temp = MIN_TEMP
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, coordinator: FlexitCoordinator) -> None:
|
||||
"""Initialize the Flexit unit."""
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from aioflo.api import API
|
||||
from aioflo.errors import RequestError
|
||||
from orjson import JSONDecodeError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@@ -18,6 +19,8 @@ from .const import DOMAIN as FLO_DOMAIN, LOGGER
|
||||
class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=hass-enforce-coordinator-module
|
||||
"""Flo device object."""
|
||||
|
||||
_failure_count: int = 0
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api_client: API, location_id: str, device_id: str
|
||||
) -> None:
|
||||
@@ -43,8 +46,11 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=
|
||||
await self.send_presence_ping()
|
||||
await self._update_device()
|
||||
await self._update_consumption_data()
|
||||
except RequestError as error:
|
||||
raise UpdateFailed(error) from error
|
||||
self._failure_count = 0
|
||||
except (RequestError, TimeoutError, JSONDecodeError) as error:
|
||||
self._failure_count += 1
|
||||
if self._failure_count > 3:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
@property
|
||||
def location_id(self) -> str:
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
from .router import get_api
|
||||
from .router import get_api, get_hosts_list_if_supported
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -69,7 +69,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# Check permissions
|
||||
await fbx.system.get_config()
|
||||
await fbx.lan.get_hosts_list()
|
||||
await get_hosts_list_if_supported(fbx)
|
||||
|
||||
# Close connection
|
||||
await fbx.close()
|
||||
|
||||
@@ -64,6 +64,33 @@ async def get_api(hass: HomeAssistant, host: str) -> Freepybox:
|
||||
return Freepybox(APP_DESC, token_file, API_VERSION)
|
||||
|
||||
|
||||
async def get_hosts_list_if_supported(
|
||||
fbx_api: Freepybox,
|
||||
) -> tuple[bool, list[dict[str, Any]]]:
|
||||
"""Hosts list is not supported when freebox is configured in bridge mode."""
|
||||
supports_hosts: bool = True
|
||||
fbx_devices: list[dict[str, Any]] = []
|
||||
try:
|
||||
fbx_devices = await fbx_api.lan.get_hosts_list() or []
|
||||
except HttpRequestError as err:
|
||||
if (
|
||||
(matcher := re.search(r"Request failed \(APIResponse: (.+)\)", str(err)))
|
||||
and is_json(json_str := matcher.group(1))
|
||||
and (json_resp := json.loads(json_str)).get("error_code") == "nodev"
|
||||
):
|
||||
# No need to retry, Host list not available
|
||||
supports_hosts = False
|
||||
_LOGGER.debug(
|
||||
"Host list is not available using bridge mode (%s)",
|
||||
json_resp.get("msg"),
|
||||
)
|
||||
|
||||
else:
|
||||
raise err
|
||||
|
||||
return supports_hosts, fbx_devices
|
||||
|
||||
|
||||
class FreeboxRouter:
|
||||
"""Representation of a Freebox router."""
|
||||
|
||||
@@ -111,27 +138,9 @@ class FreeboxRouter:
|
||||
|
||||
# Access to Host list not available in bridge mode, API return error_code 'nodev'
|
||||
if self.supports_hosts:
|
||||
try:
|
||||
fbx_devices = await self._api.lan.get_hosts_list()
|
||||
except HttpRequestError as err:
|
||||
if (
|
||||
(
|
||||
matcher := re.search(
|
||||
r"Request failed \(APIResponse: (.+)\)", str(err)
|
||||
)
|
||||
)
|
||||
and is_json(json_str := matcher.group(1))
|
||||
and (json_resp := json.loads(json_str)).get("error_code") == "nodev"
|
||||
):
|
||||
# No need to retry, Host list not available
|
||||
self.supports_hosts = False
|
||||
_LOGGER.debug(
|
||||
"Host list is not available using bridge mode (%s)",
|
||||
json_resp.get("msg"),
|
||||
)
|
||||
|
||||
else:
|
||||
raise err
|
||||
self.supports_hosts, fbx_devices = await get_hosts_list_if_supported(
|
||||
self._api
|
||||
)
|
||||
|
||||
# Adds the Freebox itself
|
||||
fbx_devices.append(
|
||||
|
||||
@@ -64,10 +64,15 @@ class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], ClimateEntity):
|
||||
_attr_hvac_modes = SUPPORTED_HVAC_MODES
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_name = None
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_current_temperature = 0
|
||||
_attr_target_temperature = 0
|
||||
_attr_hvac_mode = HVACMode.OFF
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -80,6 +80,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240131.0"]
|
||||
"requirements": ["home-assistant-frontend==20240207.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aio_georss_gdacs", "aio_georss_client"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aio-georss-gdacs==0.8"]
|
||||
"requirements": ["aio-georss-gdacs==0.9"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
@@ -140,6 +141,12 @@ class GenericCamera(Camera):
|
||||
_LOGGER.error("Error parsing template %s: %s", self._still_image_url, err)
|
||||
return self._last_image
|
||||
|
||||
try:
|
||||
vol.Schema(vol.Url())(url)
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.warning("Invalid URL '%s': %s, returning last image", url, err)
|
||||
return self._last_image
|
||||
|
||||
if url == self._last_url and self._limit_refetch:
|
||||
return self._last_image
|
||||
|
||||
|
||||
@@ -178,6 +178,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Representation of a Generic Thermostat device."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -225,7 +226,11 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
|
||||
self._target_temp = target_temp
|
||||
self._attr_temperature_unit = unit
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
self._attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
if len(presets):
|
||||
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
||||
self._attr_preset_modes = [PRESET_NONE] + list(presets.keys())
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user