Compare commits

..

89 Commits

Author SHA1 Message Date
Franck Nijhof 06ea3a3014 2024.9.3 (#126566) 2024-09-24 09:01:55 +02:00
Franck Nijhof 59ecd47374 Hotfix test for patch release in fritzbox 2024-09-23 20:09:09 +02:00
Markus Jacobsen 95053f7114 Bump mozart_api to 3.4.1.8.8 (#126334)
Update API
2024-09-23 20:07:05 +02:00
Franck Nijhof 4949727cd5 Bump version to 2024.9.3 2024-09-23 19:47:41 +02:00
Markus Jacobsen 08b0064ce7 Fix blocking call in Bang & Olufsen API client initialization (#126456)
* Update API

* Add fix for blocking call to load_default_certs
2024-09-23 19:47:30 +02:00
Steve Easley c9571126a3 Add support for new JVC Projector auth method (#126453) 2024-09-23 19:47:27 +02:00
David Knowles 06d825d6c8 Bump pydrawise to 2024.9.0 (#126431) 2024-09-23 19:47:24 +02:00
Mr. Bubbles 36e6ab4af8 Fix due date calculation for future dailies in Habitica integration (#126403)
Calculate next due date for dailies with startdate in the future
2024-09-23 19:47:21 +02:00
Manuel Frei ccec85f047 Fix surepetcare token update (#126385)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-09-23 19:47:18 +02:00
G Johansson e8a5a75e96 Bump python-holidays to 0.57 (#126367) 2024-09-23 19:47:15 +02:00
Michael 4eb1fca68e Fix next change (scheduler) sensors in AVM FRITZ!SmartHome (#126363) 2024-09-23 19:47:12 +02:00
Joost Lekkerkerker fba24b8ead Bump airgradient to 0.9.0 (#126319)
* Bump airgradient to 0.9.0

* Bump airgradient to 0.9.0
2024-09-23 19:44:22 +02:00
Matthias Alphart edfb9f3f6b Fix loading KNX UI entities with entity category set (#126290)
* Fix loading KNX UI entities with entity category set

* add test

* docstring fixes

* telegram order

* Optionally ignore telegram sending order in tests

because we can't know which platform initialises first
2024-09-23 19:44:18 +02:00
Marcel van der Veldt 2322d071e4 Fix Matter climate platform attributes when dedicated OnOff attribute is off (#126286) 2024-09-23 19:44:15 +02:00
Fredrik Erlandsson 7658ed8eaa Bump pydaikin to 2.13.7 (#126219) 2024-09-23 19:44:12 +02:00
Sebastian Nohn c81f280bc1 Fix tibber fails if power production is enabled but no power is produced (#126209)
* fix #125312 - tibber integration fails if power production is enabled but no power is produced

* fix requirements_all.txt
2024-09-23 19:44:09 +02:00
Andrew Jackson 6e36febd37 Broaden scope of ConfigEntryNotReady in Mealie (#126208)
Broaden scope of ConfigEntryNotReady
2024-09-23 19:44:06 +02:00
Jan-Philipp Benecke b38c193fe4 Prevent blocking event loop in ps4 (#126151)
* Prevent blocking event loop in ps4

* Process code review comment
2024-09-23 19:44:02 +02:00
Arun Philip b336cae118 Fix qbittorrent error when torrent count is 0 (#126146)
Fix handling of `NoneType` for torrents in `count_torrents_in_states` function

Added a check to handle cases where the 'torrents' data is None, avoiding a `TypeError` when attempting to get the length of a `NoneType` object. The function now returns 0 if 'torrents' is None, ensuring robust behavior when no torrent data is available.
2024-09-23 19:43:59 +02:00
Milan Meulemans 991114eb7f Update Aseko to support new API (#126133)
* Update Aseko to support new API

* Apply suggestions from code review

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Use self.unit instead of self._unit

* Refactor sensor setup entry

* Keep same unique id and identifier

* Revert rename free_chlorine translation key

* Remove new heating entity to keep PR small

* Fix keep same unique id

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-09-23 19:43:56 +02:00
Tobias Sauerwein d924fc5967 Fix set brightness for Netatmo lights (#126075)
* fix set brightness for Netatmo lights

* round returns int by default

* Update homeassistant/components/netatmo/light.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-09-23 19:43:50 +02:00
Brett Adams c64222de4f Fix wall connector state in Teslemetry (#124149)
* Fix wall connector state

* review feedback

* Rename None to Disconnected

* Translate disconnected
2024-09-23 10:17:38 +02:00
Franck Nijhof b69b5aa82a 2024.9.2 (#126062) 2024-09-16 19:44:37 +02:00
Jason Hunter 861fcbe598 Pin pyasn1 until fixed (#125712)
* pin pyasn1 until fixed

* add to gen requirements
2024-09-16 17:38:17 +02:00
Franck Nijhof d259055af0 Bump version to 2024.9.2 2024-09-16 16:55:02 +02:00
TimL fae26ee5da Abort zeroconf flow on connect error during discovery (#125980)
Abort zereconf flow on connect error during discovery
2024-09-16 15:58:51 +02:00
Noah Husby c4eca4469f Bump aiorussound to 3.0.5 (#125975) 2024-09-16 15:58:48 +02:00
Galorhallen d91cc96cd2 Bump govee light local to 1.5.2 (#125968)
Update govee light local library
2024-09-16 15:58:44 +02:00
starkillerOG 0b226c1868 Bump motionblinds to 0.6.25 (#125957) 2024-09-16 15:53:59 +02:00
puddly 359f61e55a Bump ZHA to 0.0.33 (#125914) 2024-09-16 15:53:55 +02:00
Josef Zweck 16e049b7fa Bump lmcloud to 1.2.3 (#125801) 2024-09-16 15:53:51 +02:00
puddly 8a6eec925f Add missing Zigbee/Thread firmware config flow translations (#125782) 2024-09-16 15:45:45 +02:00
starkillerOG f365995c8a Fix favorite position missing for Motion Blinds TDBU devices (#125750)
* Add favorite position for TDBU

* fix styling
2024-09-16 15:45:41 +02:00
Assaf Akrabi 20ded56c99 Bump russound to 0.2.0 (#125743)
* Update russound library to fix BrokenPipeError

* Remove library from license expection list
2024-09-16 15:45:38 +02:00
Matthias Alphart 4583e070df Update knx-frontend to 2024.9.10.221729 (#125734) 2024-09-16 15:45:33 +02:00
epenet d4be1f3666 Bump sfrbox-api to 0.0.11 (#125732)
* Bump sfrbox-api to 0.0.11

* Re-enable tests
2024-09-16 15:45:30 +02:00
puddly 06d4b3281b Remove unused keys from the ZHA config schema (#125710) 2024-09-16 15:45:27 +02:00
jonnynch 1dcd5471a0 Bump to python-nest-sdm to 5.0.1 (#125706) 2024-09-16 15:45:23 +02:00
Jan Bouwhuis d0b6ef877e Fix incomfort invalid setpoint if override is reported as 0.0 (#125694) 2024-09-16 15:45:20 +02:00
Steven B. 1e63b956f5 Bump tplink python-kasa lib to 0.7.3 (#125686) 2024-09-16 15:45:17 +02:00
Steven B. 7734bdfdab Update tplink config to include aes keys (#125685) 2024-09-16 15:45:13 +02:00
Paul Bottein 7eb9036cbb Update frontend to 20240909.1 (#125610) 2024-09-16 15:45:10 +02:00
Pete Sage 6b2526ddbd FIx Sonos announce regression issue (#125515)
* initial commit

* initial commit
2024-09-16 15:45:06 +02:00
G Johansson 0b1a898c7c Fix yale_smart_alarm on missing key (#125508) 2024-09-16 15:45:03 +02:00
Avi Miller fe247a60ef Bump aiolifx and aiolifx-themes to support more than 82 zones (#125487)
Signed-off-by: Avi Miller <me@dje.li>
2024-09-16 15:45:00 +02:00
J. Nick Koston 17402848f2 Bump yalexs to 8.6.4 (#125442)
adds a debounce to the updates to ensure we do
not request the activities api too often if
the websocket sends rapid updates

fixes #125277
2024-09-16 15:44:56 +02:00
tmenguy e6b4c2e700 Fix renault plug state (#125421)
* Added PlugState 3, that is coming with renault-api 0.2.7, it fixes #124682 HA ticket

* Added PlugState 3, that is coming with renault-api 0.2.7, it fixes #124682 HA ticket
2024-09-16 15:44:52 +02:00
Willem-Jan van Rootselaar e7c48d5870 Update diagnostics for BSBLan (#124508)
* update diagnostics to include static

and make room for multiple coordinator data objects

* fix mac address is not stored in config_entry but on device
2024-09-16 15:42:11 +02:00
Michał Jaworski 781342be40 Fix mired range in blebox color temp mode lights (#124258)
* fix: use default mired range in belbox lights running in color temp mode

* fix: ruff
2024-09-16 15:42:08 +02:00
David Knowles 73b26407f6 Fix Schlage removed locks (#123627)
* Fix bugs when a lock is no longer returned by the API

* Changes requested during review

* Only mark unavailable if lock is not present

* Remove stale comment

* Remove over-judicious nullability checks

* Remove another unnecessary null check
2024-09-16 15:42:05 +02:00
Simon b1d691178e Use default voice id as fallback in get_tts_audio (#123624) 2024-09-16 15:42:02 +02:00
Kristof Mattei dc189e1d58 Fix Lyric climate Auto mode (#123490)
fix: Lyric has an actual "Auto" mode that is exposed if the device has an Auto mode.
2024-09-16 15:41:52 +02:00
Paulus Schoutsen 444560543c 2024.9.1 (#125420) 2024-09-06 13:39:52 -04:00
Paulus Schoutsen ed2d321746 Bump version to 2024.9.1 2024-09-06 14:57:08 +00:00
Pierre Ståhl b50d8fca16 Bump pyatv to 0.15.1 (#125412) 2024-09-06 14:57:02 +00:00
Paul Bottein 1c7c6d6592 Update frontend to 20240906.0 (#125409) 2024-09-06 14:57:01 +00:00
Marlon 5cf89bf2bb Set min_power similar to max_power to support all inverters from apsystems (#124247)
Set min_power similar to max_power to support all inverters from apsystems ez1 series
2024-09-06 14:57:00 +00:00
TimL 0b95cf1251 Improve handling of old firmware versions (#125406)
* Update Info fixture with new fields from pysmlight 0.0.14

* Create repair if device is running unsupported firmware

* Add test for legacy firmware info

* Add strings for repair issue
2024-09-06 14:56:06 +00:00
Alexandre TRUPIN a3f42e36ac Bump sfrbox-api to 0.0.10 (#125405)
* bump sfr_box requirement to 0.0.10

* upate manifest file

* Handle None values

---------

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-09-06 14:53:07 +00:00
Dave Leaver 973e43ae6a Fix controlling AC temperature in airtouch5 (#125394)
Fix controlling AC temperature
2024-09-06 14:53:06 +00:00
dontinelli e80e189e6b Increase coordinator update_interval for fyta (#125393)
* Increase update_interval

* Update homeassistant/components/fyta/coordinator.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-09-06 14:53:05 +00:00
Andre Lengwenus 27dc2e1b9d Bump pypck to 0.7.22 (#125389) 2024-09-06 14:53:04 +00:00
TimL edb7c76caa Bump pysmlight to 0.0.14 (#125387)
Bump pysmlight 0.0.14 for smlight
2024-09-06 14:53:03 +00:00
Ryan Mattson 7859d31ca0 Lyric: fixed missed snake case conversions (#125382)
fixed missed snake case conversions
2024-09-06 14:53:02 +00:00
Marcel van der Veldt 6c640d2abe Fix for Hue sending effect None at turn_on command while no effect is active (#125377)
* Fix for Hue sending effect None at turn_on command while no effect is active

* typo

* update tests
2024-09-06 14:53:01 +00:00
Erik Montnemery 61ee3a9412 Don't allow templating min, max, step in config entry template number (#125342) 2024-09-06 14:53:00 +00:00
Simon Lamon 4ed18495f3 Add follower to the PlayingMode enum (#125294)
Update media_player.py
2024-09-06 14:52:59 +00:00
Noah Husby 5c8b2cde92 Bump aiorussound to 3.0.4 (#125285)
feat: bump aiorussound to 3.0.4
2024-09-06 14:52:58 +00:00
Jordi 5c2073481d Increase AquaCell timeout and handle timeout exception properly (#125263)
* Increase timeout and add handling of timeout exception

* Raise update failed instead of config entry error
2024-09-06 14:52:57 +00:00
G Johansson 84c204a7b3 Don't show input panel if default code provided in envisalink (#125256) 2024-09-06 14:52:56 +00:00
G Johansson 6c15f251c6 Fix blocking call in yale_smart_alarm (#125255) 2024-09-06 14:52:56 +00:00
Joost Lekkerkerker 48fcf58eb9 Revert #122676 Yamaha discovery (#125216)
Revert Yamaha discovery
2024-09-06 14:52:55 +00:00
Simon Lamon 3c13f4b4cc Improve play media support in LinkPlay (#125205)
Improve play media support in linkplay
2024-09-06 14:52:54 +00:00
Shay Levy a14826d75e Fix BTHome validate triggers for device with multiple buttons (#125183)
* Fix BTHome validate triggers for device with multiple buttons

* Remove None default
2024-09-06 14:52:53 +00:00
Bram Kragten 36ec1b33fe 2024.9.0 (#124880) 2024-09-04 17:41:08 +02:00
Bram Kragten 84a0a28be2 Bump version to 2024.9.0 2024-09-04 17:08:18 +02:00
Michael Hansen ac19ee3e2e Bump intents to 2024.9.4 (#125232) 2024-09-04 17:07:21 +02:00
Denis Shulyaka 438af042ed Update Anthropic default model to Haiku (#125225) 2024-09-04 17:07:20 +02:00
Robert Resch 122f11c790 Update modified_at datetime on storage collection changes (#125218) 2024-09-04 17:07:19 +02:00
Bram Kragten de99dfef4e Bump version to 2024.9.0b5 2024-09-04 11:48:24 +02:00
Robert Resch bcdc3563a5 Bump deebot-client to 8.4.0 (#125207) 2024-09-04 11:48:16 +02:00
Bram Kragten 9ef0a1f0a2 Update frontend to 20240904.0 (#125206) 2024-09-04 11:48:15 +02:00
Matthias Alphart d0629d4e66 Update knx-frontend to 2024.9.4.64538 (#125196) 2024-09-04 11:48:15 +02:00
G Johansson 65e98eab9c Bump python-holidays to 0.56 (#125182) 2024-09-04 11:48:14 +02:00
J. Nick Koston a0d9764443 Bump yalexs to 8.6.3 (#125176)
Fixes the battery state not refreshing due to a refactoring
error in the library.

changelog: https://github.com/bdraco/yalexs/compare/v8.6.2...v8.6.3
2024-09-04 11:48:13 +02:00
Joakim Plate 8293f270df Update gardena_bluetooth dependency to 1.4.3 (#125175) 2024-09-04 11:48:12 +02:00
Paulus Schoutsen 516f3295bf 2024.8.3 (#124569) 2024-08-25 16:06:09 +02:00
Franck Nijhof 94516de724 2024.8.2 (#124069) 2024-08-16 18:43:41 +02:00
Franck Nijhof ae4fc9504a 2024.8.1 (#123544) 2024-08-10 19:32:02 +02:00
162 changed files with 3387 additions and 1978 deletions
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/airgradient",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["airgradient==0.8.0"],
"requirements": ["airgradient==0.9.0"],
"zeroconf": ["_airgradient._tcp.local."]
}
@@ -262,7 +262,7 @@ class Airtouch5AC(Airtouch5ClimateEntity):
_LOGGER.debug("Argument `temperature` is missing in set_temperature")
return
await self._control(temp=temp)
await self._control(setpoint=SetpointControl.CHANGE_SETPOINT, temp=temp)
class Airtouch5Zone(Airtouch5ClimateEntity):
+1 -1
View File
@@ -8,7 +8,7 @@ LOGGER = logging.getLogger(__package__)
CONF_RECOMMENDED = "recommended"
CONF_PROMPT = "prompt"
CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "claude-3-5-sonnet-20240620"
RECOMMENDED_CHAT_MODEL = "claude-3-haiku-20240307"
CONF_MAX_TOKENS = "max_tokens"
RECOMMENDED_MAX_TOKENS = 1024
CONF_TEMPERATURE = "temperature"
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.15.0"],
"requirements": ["pyatv==0.15.1"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",
@@ -36,10 +36,11 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
async def _async_setup(self) -> None:
try:
max_power = (await self.api.get_device_info()).maxPower
device_info = await self.api.get_device_info()
except (ConnectionError, TimeoutError):
raise UpdateFailed from None
self.api.max_power = max_power
self.api.max_power = device_info.maxPower
self.api.min_power = device_info.minPower
async def _async_update_data(self) -> ApSystemsSensorData:
output_data = await self.api.get_output_data()
+1 -1
View File
@@ -26,7 +26,6 @@ async def async_setup_entry(
class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
"""Base sensor to be used with description."""
_attr_native_min_value = 30
_attr_native_step = 1
_attr_device_class = NumberDeviceClass.POWER
_attr_mode = NumberMode.BOX
@@ -42,6 +41,7 @@ class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
self._api = data.coordinator.api
self._attr_unique_id = f"{data.device_id}_output_limit"
self._attr_native_max_value = data.coordinator.api.max_power
self._attr_native_min_value = data.coordinator.api.min_power
async def async_update(self) -> None:
"""Set the state with the value fetched from the inverter."""
@@ -56,7 +56,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
refresh_token = await api.authenticate(
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except ApiException:
except (ApiException, TimeoutError):
errors["base"] = "cannot_connect"
except AuthenticationFailed:
errors["base"] = "invalid_auth"
@@ -56,7 +56,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
so entities can quickly look up their data.
"""
async with asyncio.timeout(10):
async with asyncio.timeout(30):
# Check if the refresh token is expired
expiry_time = (
self.refresh_token_creation_time
@@ -72,7 +72,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
softeners = await self.aquacell_api.get_all_softeners()
except AuthenticationFailed as err:
raise ConfigEntryError from err
except AquacellApiException as err:
except (AquacellApiException, TimeoutError) as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
return {softener.dsn: softener for softener in softeners}
@@ -4,13 +4,12 @@ from __future__ import annotations
import logging
from aioaseko import APIUnavailable, InvalidAuthCredentials, MobileAccount
from aioaseko import Aseko, AsekoNotLoggedIn
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import DOMAIN
from .coordinator import AsekoDataUpdateCoordinator
@@ -22,28 +21,17 @@ PLATFORMS: list[str] = [Platform.BINARY_SENSOR, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Aseko Pool Live from a config entry."""
account = MobileAccount(
async_get_clientsession(hass),
username=entry.data[CONF_EMAIL],
password=entry.data[CONF_PASSWORD],
)
aseko = Aseko(entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD])
try:
units = await account.get_units()
except InvalidAuthCredentials as err:
await aseko.login()
except AsekoNotLoggedIn as err:
raise ConfigEntryAuthFailed from err
except APIUnavailable as err:
raise ConfigEntryNotReady from err
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = []
for unit in units:
coordinator = AsekoDataUpdateCoordinator(hass, unit)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id].append((unit, coordinator))
coordinator = AsekoDataUpdateCoordinator(hass, aseko)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -51,7 +39,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
@@ -8,7 +8,6 @@ from dataclasses import dataclass
from aioaseko import Unit
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
@@ -25,26 +24,14 @@ from .entity import AsekoEntity
class AsekoBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes an Aseko binary sensor entity."""
value_fn: Callable[[Unit], bool]
value_fn: Callable[[Unit], bool | None]
UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
AsekoBinarySensorEntityDescription(
key="water_flow",
translation_key="water_flow",
value_fn=lambda unit: unit.water_flow,
),
AsekoBinarySensorEntityDescription(
key="has_alarm",
translation_key="alarm",
value_fn=lambda unit: unit.has_alarm,
device_class=BinarySensorDeviceClass.SAFETY,
),
AsekoBinarySensorEntityDescription(
key="has_error",
translation_key="error",
value_fn=lambda unit: unit.has_error,
device_class=BinarySensorDeviceClass.PROBLEM,
translation_key="water_flow_to_probes",
value_fn=lambda unit: unit.water_flow_to_probes,
),
)
@@ -55,33 +42,22 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Aseko Pool Live binary sensors."""
data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][
config_entry.entry_id
]
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
units = coordinator.data.values()
async_add_entities(
AsekoUnitBinarySensorEntity(unit, coordinator, description)
for unit, coordinator in data
for description in UNIT_BINARY_SENSORS
AsekoBinarySensorEntity(unit, coordinator, description)
for description in BINARY_SENSORS
for unit in units
if description.value_fn(unit) is not None
)
class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity):
"""Representation of a unit water flow binary sensor entity."""
class AsekoBinarySensorEntity(AsekoEntity, BinarySensorEntity):
"""Representation of an Aseko binary sensor entity."""
entity_description: AsekoBinarySensorEntityDescription
def __init__(
self,
unit: Unit,
coordinator: AsekoDataUpdateCoordinator,
entity_description: AsekoBinarySensorEntityDescription,
) -> None:
"""Initialize the unit binary sensor."""
super().__init__(unit, coordinator)
self.entity_description = entity_description
self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}"
@property
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._unit)
return self.entity_description.value_fn(self.unit)
@@ -6,12 +6,11 @@ from collections.abc import Mapping
import logging
from typing import Any
from aioaseko import APIUnavailable, InvalidAuthCredentials, WebAccount
from aioaseko import Aseko, AsekoAPIError, AsekoInvalidCredentials
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
@@ -34,15 +33,12 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
async def get_account_info(self, email: str, password: str) -> dict:
"""Get account info from the mobile API and the web API."""
session = async_get_clientsession(self.hass)
web_account = WebAccount(session, email, password)
web_account_info = await web_account.login()
aseko = Aseko(email, password)
user = await aseko.login()
return {
CONF_EMAIL: email,
CONF_PASSWORD: password,
CONF_UNIQUE_ID: web_account_info.user_id,
CONF_UNIQUE_ID: user.user_id,
}
async def async_step_user(
@@ -58,9 +54,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
info = await self.get_account_info(
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except APIUnavailable:
except AsekoAPIError:
errors["base"] = "cannot_connect"
except InvalidAuthCredentials:
except AsekoInvalidCredentials:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
@@ -122,9 +118,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
info = await self.get_account_info(
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except APIUnavailable:
except AsekoAPIError:
errors["base"] = "cannot_connect"
except InvalidAuthCredentials:
except AsekoInvalidCredentials:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
@@ -5,34 +5,31 @@ from __future__ import annotations
from datetime import timedelta
import logging
from aioaseko import Unit, Variable
from aioaseko import Aseko, Unit
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Variable]]):
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Unit]]):
"""Class to manage fetching Aseko unit data from single endpoint."""
def __init__(self, hass: HomeAssistant, unit: Unit) -> None:
def __init__(self, hass: HomeAssistant, aseko: Aseko) -> None:
"""Initialize global Aseko unit data updater."""
self._unit = unit
if self._unit.name:
name = self._unit.name
else:
name = f"{self._unit.type}-{self._unit.serial_number}"
self._aseko = aseko
super().__init__(
hass,
_LOGGER,
name=name,
name=DOMAIN,
update_interval=timedelta(minutes=2),
)
async def _async_update_data(self) -> dict[str, Variable]:
async def _async_update_data(self) -> dict[str, Unit]:
"""Fetch unit data."""
await self._unit.get_state()
return {variable.type: variable for variable in self._unit.variables}
units = await self._aseko.get_units()
return {unit.serial_number: unit for unit in units}
@@ -3,6 +3,7 @@
from aioaseko import Unit
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
@@ -14,20 +15,44 @@ class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]):
_attr_has_entity_name = True
def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None:
def __init__(
self,
unit: Unit,
coordinator: AsekoDataUpdateCoordinator,
description: EntityDescription,
) -> None:
"""Initialize the aseko entity."""
super().__init__(coordinator)
self.entity_description = description
self._unit = unit
if self._unit.type == "Remote":
self._device_model = "ASIN Pool"
else:
self._device_model = f"ASIN AQUA {self._unit.type}"
self._device_name = self._unit.name if self._unit.name else self._device_model
self._attr_unique_id = f"{self.unit.serial_number}{self.entity_description.key}"
self._attr_device_info = DeviceInfo(
name=self._device_name,
identifiers={(DOMAIN, str(self._unit.serial_number))},
manufacturer="Aseko",
model=self._device_model,
identifiers={(DOMAIN, self.unit.serial_number)},
serial_number=self.unit.serial_number,
name=unit.name or unit.serial_number,
manufacturer=(
self.unit.brand_name.primary
if self.unit.brand_name is not None
else None
),
model=(
self.unit.brand_name.secondary
if self.unit.brand_name is not None
else None
),
configuration_url=f"https://aseko.cloud/unit/{self.unit.serial_number}",
)
@property
def unit(self) -> Unit:
"""Return the aseko unit."""
return self.coordinator.data[self._unit.serial_number]
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available
and self.unit.serial_number in self.coordinator.data
and self.unit.online
)
@@ -1,16 +1,25 @@
{
"entity": {
"binary_sensor": {
"water_flow": {
"water_flow_to_probes": {
"default": "mdi:waves-arrow-right"
}
},
"sensor": {
"air_temperature": {
"default": "mdi:thermometer-lines"
},
"free_chlorine": {
"default": "mdi:flask"
"default": "mdi:pool"
},
"redox": {
"default": "mdi:pool"
},
"salinity": {
"default": "mdi:pool"
},
"water_temperature": {
"default": "mdi:coolant-temperature"
"default": "mdi:pool-thermometer"
}
}
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
"iot_class": "cloud_polling",
"loggers": ["aioaseko"],
"requirements": ["aioaseko==0.2.0"]
"requirements": ["aioaseko==1.0.0"]
}
@@ -2,77 +2,104 @@
from __future__ import annotations
from aioaseko import Unit, Variable
from collections.abc import Callable
from dataclasses import dataclass
from aioaseko import Unit
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfElectricPotential, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DOMAIN
from .coordinator import AsekoDataUpdateCoordinator
from .entity import AsekoEntity
@dataclass(frozen=True, kw_only=True)
class AsekoSensorEntityDescription(SensorEntityDescription):
"""Describes an Aseko sensor entity."""
value_fn: Callable[[Unit], StateType]
SENSORS: list[AsekoSensorEntityDescription] = [
AsekoSensorEntityDescription(
key="airTemp",
translation_key="air_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda unit: unit.air_temperature,
),
AsekoSensorEntityDescription(
key="free_chlorine",
translation_key="free_chlorine",
native_unit_of_measurement="mg/l",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda unit: unit.cl_free,
),
AsekoSensorEntityDescription(
key="ph",
device_class=SensorDeviceClass.PH,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda unit: unit.ph,
),
AsekoSensorEntityDescription(
key="rx",
translation_key="redox",
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda unit: unit.redox,
),
AsekoSensorEntityDescription(
key="salinity",
translation_key="salinity",
native_unit_of_measurement="kg/m³",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda unit: unit.salinity,
),
AsekoSensorEntityDescription(
key="waterTemp",
translation_key="water_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda unit: unit.water_temperature,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Aseko Pool Live sensors."""
data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][
config_entry.entry_id
]
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
units = coordinator.data.values()
async_add_entities(
VariableSensorEntity(unit, variable, coordinator)
for unit, coordinator in data
for variable in unit.variables
AsekoSensorEntity(unit, coordinator, description)
for description in SENSORS
for unit in units
if description.value_fn(unit) is not None
)
class VariableSensorEntity(AsekoEntity, SensorEntity):
"""Representation of a unit variable sensor entity."""
class AsekoSensorEntity(AsekoEntity, SensorEntity):
"""Representation of an Aseko unit sensor entity."""
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(
self, unit: Unit, variable: Variable, coordinator: AsekoDataUpdateCoordinator
) -> None:
"""Initialize the variable sensor."""
super().__init__(unit, coordinator)
self._variable = variable
translation_key = {
"Air temp.": "air_temperature",
"Cl free": "free_chlorine",
"Water temp.": "water_temperature",
}.get(self._variable.name)
if translation_key is not None:
self._attr_translation_key = translation_key
else:
self._attr_name = self._variable.name
self._attr_unique_id = f"{self._unit.serial_number}{self._variable.type}"
self._attr_native_unit_of_measurement = self._variable.unit
self._attr_icon = {
"rx": "mdi:test-tube",
"waterLevel": "mdi:waves",
}.get(self._variable.type)
self._attr_device_class = {
"airTemp": SensorDeviceClass.TEMPERATURE,
"waterTemp": SensorDeviceClass.TEMPERATURE,
"ph": SensorDeviceClass.PH,
}.get(self._variable.type)
entity_description: AsekoSensorEntityDescription
@property
def native_value(self) -> int | None:
def native_value(self) -> StateType:
"""Return the state of the sensor."""
variable = self.coordinator.data[self._variable.type]
return variable.current_value
return self.entity_description.value_fn(self.unit)
@@ -26,11 +26,8 @@
},
"entity": {
"binary_sensor": {
"water_flow": {
"name": "Water flow"
},
"alarm": {
"name": "Alarm"
"water_flow_to_probes": {
"name": "Water flow to probes"
}
},
"sensor": {
@@ -40,6 +37,12 @@
"free_chlorine": {
"name": "Free chlorine"
},
"redox": {
"name": "Redox potential"
},
"salinity": {
"name": "Salinity"
},
"water_temperature": {
"name": "Water temperature"
}
@@ -24,5 +24,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==8.6.2", "yalexs-ble==2.4.3"]
"requirements": ["yalexs==8.6.4", "yalexs-ble==2.4.3"]
}
@@ -17,6 +17,7 @@ from homeassistant.const import CONF_HOST, CONF_MODEL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.device_registry as dr
from homeassistant.util.ssl import get_default_context
from .const import DOMAIN
from .websocket import BangOlufsenWebsocket
@@ -48,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
model=entry.data[CONF_MODEL],
)
client = MozartClient(host=entry.data[CONF_HOST])
client = MozartClient(host=entry.data[CONF_HOST], ssl_context=get_default_context())
# Check API and WebSocket connection
try:
@@ -14,6 +14,7 @@ from homeassistant.components.zeroconf import ZeroconfServiceInfo
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_MODEL
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
from homeassistant.util.ssl import get_default_context
from .const import (
ATTR_FRIENDLY_NAME,
@@ -87,7 +88,9 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
errors={"base": _exception_map[type(error)]},
)
self._client = MozartClient(self._host)
self._client = MozartClient(
host=self._host, ssl_context=get_default_context()
)
# Try to get information from Beolink self method.
async with self._client:
@@ -136,7 +139,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="ipv6_address")
# Check connection to ensure valid address is received
self._client = MozartClient(self._host)
self._client = MozartClient(self._host, ssl_context=get_default_context())
async with self._client:
try:
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["mozart-api==3.4.1.8.6"],
"requirements": ["mozart-api==3.4.1.8.8"],
"zeroconf": ["_bangolufsen._tcp.local."]
}
+4 -6
View File
@@ -60,6 +60,9 @@ COLOR_MODE_MAP = {
class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
"""Representation of BleBox lights."""
_attr_max_mireds = 370 # 1,000,000 divided by 2700 Kelvin = 370 Mireds
_attr_min_mireds = 154 # 1,000,000 divided by 6500 Kelvin = 154 Mireds
def __init__(self, feature: blebox_uniapi.light.Light) -> None:
"""Initialize a BleBox light."""
super().__init__(feature)
@@ -87,12 +90,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
Set values to _attr_ibutes if needed.
"""
color_mode_tmp = COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF)
if color_mode_tmp == ColorMode.COLOR_TEMP:
self._attr_min_mireds = 1
self._attr_max_mireds = 255
return color_mode_tmp
return COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF)
@property
def supported_color_modes(self):
@@ -20,5 +20,8 @@ async def async_get_config_entry_diagnostics(
return {
"info": data.info.to_dict(),
"device": data.device.to_dict(),
"state": data.coordinator.data.state.to_dict(),
"coordinator_data": {
"state": data.coordinator.data.state.to_dict(),
},
"static": data.static.to_dict(),
}
+3 -3
View File
@@ -22,10 +22,10 @@ class BSBLanEntity(CoordinatorEntity[BSBLanUpdateCoordinator]):
def __init__(self, coordinator: BSBLanUpdateCoordinator, data: BSBLanData) -> None:
"""Initialize BSBLan entity."""
super().__init__(coordinator, data)
host = self.coordinator.config_entry.data["host"]
mac = self.coordinator.config_entry.data["mac"]
host = coordinator.config_entry.data["host"]
mac = data.device.MAC
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, data.device.MAC)},
identifiers={(DOMAIN, mac)},
connections={(CONNECTION_NETWORK_MAC, format_mac(mac))},
name=data.device.name,
manufacturer="BSBLAN Inc.",
@@ -7,6 +7,9 @@ from typing import Any
import voluptuous as vol
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
from homeassistant.components.homeassistant.triggers import event as event_trigger
from homeassistant.const import (
CONF_DEVICE_ID,
@@ -43,33 +46,46 @@ TRIGGERS_BY_EVENT_CLASS = {
EVENT_CLASS_DIMMER: {"rotate_left", "rotate_right"},
}
SCHEMA_BY_EVENT_CLASS = {
EVENT_CLASS_BUTTON: DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_BUTTON]),
vol.Required(CONF_SUBTYPE): vol.In(
TRIGGERS_BY_EVENT_CLASS[EVENT_CLASS_BUTTON]
),
}
),
EVENT_CLASS_DIMMER: DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_DIMMER]),
vol.Required(CONF_SUBTYPE): vol.In(
TRIGGERS_BY_EVENT_CLASS[EVENT_CLASS_DIMMER]
),
}
),
}
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str}
)
async def async_validate_trigger_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate trigger config."""
return SCHEMA_BY_EVENT_CLASS.get(config[CONF_TYPE], DEVICE_TRIGGER_BASE_SCHEMA)( # type: ignore[no-any-return]
config
config = TRIGGER_SCHEMA(config)
event_class = config[CONF_TYPE]
event_type = config[CONF_SUBTYPE]
device_registry = dr.async_get(hass)
device = device_registry.async_get(config[CONF_DEVICE_ID])
assert device is not None
config_entries = [
hass.config_entries.async_get_entry(entry_id)
for entry_id in device.config_entries
]
bthome_config_entry = next(
iter(entry for entry in config_entries if entry and entry.domain == DOMAIN)
)
event_classes: list[str] = bthome_config_entry.data.get(
CONF_DISCOVERED_EVENT_CLASSES, []
)
if event_class not in event_classes:
raise InvalidDeviceAutomationConfig(
f"BTHome trigger {event_class} is not valid for device "
f"{device} ({config[CONF_DEVICE_ID]})"
)
if event_type not in TRIGGERS_BY_EVENT_CLASS.get(event_class.split("_")[0], ()):
raise InvalidDeviceAutomationConfig(
f"BTHome trigger {event_type} is not valid for device "
f"{device} ({config[CONF_DEVICE_ID]})"
)
return config
async def async_get_triggers(
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.8.29"]
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.9.4"]
}
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/daikin",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.13.6"],
"requirements": ["pydaikin==2.13.7"],
"zeroconf": ["_dkapi._tcp.local."]
}
@@ -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.10", "deebot-client==8.3.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==8.4.0"]
}
+1 -1
View File
@@ -100,7 +100,7 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
"""Load tts audio file from the engine."""
_LOGGER.debug("Getting TTS audio for %s", message)
_LOGGER.debug("Options: %s", options)
voice_id = options[ATTR_VOICE]
voice_id = options.get(ATTR_VOICE, self._default_voice_id)
try:
audio = await self._client.generate(
text=message,
@@ -119,7 +119,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
self._partition_number = partition_number
self._panic_type = panic_type
self._alarm_control_panel_option_default_code = code
self._attr_code_format = CodeFormat.NUMBER
self._attr_code_format = CodeFormat.NUMBER if not code else None
_LOGGER.debug("Setting up alarm: %s", alarm_name)
super().__init__(alarm_name, info, controller)
+22 -4
View File
@@ -83,20 +83,38 @@ def entity_category_temperature(device: FritzhomeDevice) -> EntityCategory | Non
return None
def value_nextchange_preset(device: FritzhomeDevice) -> str:
def value_nextchange_preset(device: FritzhomeDevice) -> str | None:
"""Return native value for next scheduled preset sensor."""
if not device.nextchange_endperiod:
return None
if device.nextchange_temperature == device.eco_temperature:
return PRESET_ECO
return PRESET_COMFORT
def value_scheduled_preset(device: FritzhomeDevice) -> str:
def value_scheduled_preset(device: FritzhomeDevice) -> str | None:
"""Return native value for current scheduled preset sensor."""
if not device.nextchange_endperiod:
return None
if device.nextchange_temperature == device.eco_temperature:
return PRESET_COMFORT
return PRESET_ECO
def value_nextchange_temperature(device: FritzhomeDevice) -> float | None:
"""Return native value for next scheduled temperature time sensor."""
if device.nextchange_endperiod and isinstance(device.nextchange_temperature, float):
return device.nextchange_temperature
return None
def value_nextchange_time(device: FritzhomeDevice) -> datetime | None:
"""Return native value for next scheduled changed time sensor."""
if device.nextchange_endperiod:
return utc_from_timestamp(device.nextchange_endperiod)
return None
SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
FritzSensorEntityDescription(
key="temperature",
@@ -181,7 +199,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
suitable=suitable_nextchange_temperature,
native_value=lambda device: device.nextchange_temperature,
native_value=value_nextchange_temperature,
),
FritzSensorEntityDescription(
key="nextchange_time",
@@ -189,7 +207,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
suitable=suitable_nextchange_time,
native_value=lambda device: utc_from_timestamp(device.nextchange_endperiod),
native_value=value_nextchange_time,
),
FritzSensorEntityDescription(
key="nextchange_preset",
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20240903.1"]
"requirements": ["home-assistant-frontend==20240909.1"]
}
+1 -1
View File
@@ -39,7 +39,7 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]):
hass,
_LOGGER,
name="FYTA Coordinator",
update_interval=timedelta(seconds=60),
update_interval=timedelta(minutes=4),
)
self.fyta = fyta
@@ -14,5 +14,5 @@
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
"iot_class": "local_polling",
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
"requirements": ["gardena-bluetooth==1.4.2"]
"requirements": ["gardena-bluetooth==1.4.3"]
}
@@ -6,5 +6,5 @@
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"iot_class": "local_push",
"requirements": ["govee-local-api==1.5.1"]
"requirements": ["govee-local-api==1.5.2"]
}
+30 -11
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
import datetime
from typing import Any
from typing import TYPE_CHECKING, Any
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
@@ -14,25 +14,44 @@ from homeassistant.util import dt as dt_util
def next_due_date(task: dict[str, Any], last_cron: str) -> datetime.date | None:
"""Calculate due date for dailies and yesterdailies."""
today = to_date(last_cron)
startdate = to_date(task["startDate"])
if TYPE_CHECKING:
assert today
assert startdate
if task["isDue"] and not task["completed"]:
return dt_util.as_local(datetime.datetime.fromisoformat(last_cron)).date()
return to_date(last_cron)
if startdate > today:
if task["frequency"] == "daily" or (
task["frequency"] in ("monthly", "yearly") and task["daysOfMonth"]
):
return startdate
if (
task["frequency"] in ("weekly", "monthly")
and (nextdue := to_date(task["nextDue"][0]))
and startdate > nextdue
):
return to_date(task["nextDue"][1])
return to_date(task["nextDue"][0])
def to_date(date: str) -> datetime.date | None:
"""Convert an iso date to a datetime.date object."""
try:
return dt_util.as_local(
datetime.datetime.fromisoformat(task["nextDue"][0])
).date()
return dt_util.as_local(datetime.datetime.fromisoformat(date)).date()
except ValueError:
# sometimes nextDue dates are in this format instead of iso:
# sometimes nextDue dates are JavaScript datetime strings instead of iso:
# "Mon May 06 2024 00:00:00 GMT+0200"
try:
return dt_util.as_local(
datetime.datetime.strptime(
task["nextDue"][0], "%a %b %d %Y %H:%M:%S %Z%z"
)
datetime.datetime.strptime(date, "%a %b %d %Y %H:%M:%S %Z%z")
).date()
except ValueError:
return None
except IndexError:
return None
def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.55", "babel==2.15.0"]
"requirements": ["holidays==0.57", "babel==2.15.0"]
}
@@ -51,7 +51,8 @@
"not_hassio_thread": "The OpenThread Border Router addon can only be installed with Home Assistant OS. If you would like to use the {model} as an Thread border router, please flash the firmware manually using the [web flasher]({docs_web_flasher_url}) and set up OpenThread Border Router to communicate with it.",
"otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again.",
"zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.",
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again."
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.",
"unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or addon is currently trying to communicate with the device. If you are running Home Assistant OS in a virtual machine or in Docker, please make sure that permissions are set correctly for the device."
},
"progress": {
"install_zigbee_flasher_addon": "The Silicon Labs Flasher addon is installed, this may take a few minutes.",
@@ -113,7 +113,8 @@
"not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]",
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]"
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
"unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]"
},
"progress": {
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
@@ -181,7 +182,10 @@
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]",
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
"not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]",
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]"
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
"unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]"
},
"progress": {
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
@@ -138,7 +138,8 @@
"not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]",
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]"
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
"unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or addon is currently trying to communicate with the device."
},
"progress": {
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
+5 -1
View File
@@ -226,7 +226,11 @@ class HueLight(HueBaseEntity, LightEntity):
flash = kwargs.get(ATTR_FLASH)
effect = effect_str = kwargs.get(ATTR_EFFECT)
if effect_str in (EFFECT_NONE, EFFECT_NONE.lower()):
effect = EffectStatus.NO_EFFECT
# ignore effect if set to "None" and we have no effect active
# the special effect "None" is only used to stop an active effect
# but sending it while no effect is active can actually result in issues
# https://github.com/home-assistant/core/issues/122165
effect = None if self.effect == EFFECT_NONE else EffectStatus.NO_EFFECT
elif effect_str is not None:
# work out if we got a regular effect or timed effect
effect = EffectStatus(effect_str)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2024.8.0"]
"requirements": ["pydrawise==2024.9.0"]
}
@@ -90,8 +90,10 @@ class InComfortClimate(IncomfortEntity, ClimateEntity):
As we set the override, we report back the override. The actual set point is
is returned at a later time.
Some older thermostats return 0.0 as override, in that case we fallback to
the actual setpoint.
"""
return self._room.override
return self._room.override or self._room.setpoint
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set a new target temperature for this zone."""
@@ -4,6 +4,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from jvcprojector import (
JvcProjector,
@@ -40,7 +41,7 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
self.device = device
self.unique_id = format_mac(device.mac)
async def _async_update_data(self) -> dict[str, str]:
async def _async_update_data(self) -> dict[str, Any]:
"""Get the latest state data."""
try:
state = await self.device.get_state()
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["jvcprojector"],
"requirements": ["pyjvcprojector==1.0.12"]
"requirements": ["pyjvcprojector==1.1.0"]
}
+15 -6
View File
@@ -2,20 +2,23 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
from xknx.devices import Device as XknxDevice
from homeassistant.const import CONF_ENTITY_CATEGORY, EntityCategory
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.entity_registry import RegistryEntry
from .const import DOMAIN
from .storage.config_store import PlatformControllerBase
from .storage.const import CONF_DEVICE_INFO
if TYPE_CHECKING:
from . import KNXModule
from .storage.config_store import PlatformControllerBase
class KnxUiEntityPlatformController(PlatformControllerBase):
"""Class to manage dynamic adding and reloading of UI entities."""
@@ -93,13 +96,19 @@ class KnxYamlEntity(_KnxEntityBase):
self._device = device
class KnxUiEntity(_KnxEntityBase, ABC):
class KnxUiEntity(_KnxEntityBase):
"""Representation of a KNX UI entity."""
_attr_unique_id: str
_attr_has_entity_name = True
@abstractmethod
def __init__(
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
self, knx_module: KNXModule, unique_id: str, entity_config: dict[str, Any]
) -> None:
"""Initialize the UI entity."""
self._knx_module = knx_module
self._attr_unique_id = unique_id
if entity_category := entity_config.get(CONF_ENTITY_CATEGORY):
self._attr_entity_category = EntityCategory(entity_category)
if device_info := entity_config.get(CONF_DEVICE_INFO):
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)})
+5 -9
View File
@@ -20,7 +20,6 @@ from homeassistant.components.light import (
)
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
@@ -35,7 +34,6 @@ from .schema import LightSchema
from .storage.const import (
CONF_COLOR_TEMP_MAX,
CONF_COLOR_TEMP_MIN,
CONF_DEVICE_INFO,
CONF_DPT,
CONF_ENTITY,
CONF_GA_BLUE_BRIGHTNESS,
@@ -554,21 +552,19 @@ class KnxYamlLight(_KnxLight, KnxYamlEntity):
class KnxUiLight(_KnxLight, KnxUiEntity):
"""Representation of a KNX light."""
_attr_has_entity_name = True
_device: XknxLight
def __init__(
self, knx_module: KNXModule, unique_id: str, config: ConfigType
) -> None:
"""Initialize of KNX light."""
self._knx_module = knx_module
super().__init__(
knx_module=knx_module,
unique_id=unique_id,
entity_config=config[CONF_ENTITY],
)
self._device = _create_ui_light(
knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME]
)
self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX]
self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN]
self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY]
self._attr_unique_id = unique_id
if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO):
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)})
+1 -1
View File
@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.1.1",
"xknxproject==3.7.1",
"knx-frontend==2024.8.9.225351"
"knx-frontend==2024.9.10.221729"
],
"single_config_entry": true
}
@@ -38,7 +38,10 @@ def parse_invalid(exc: vol.Invalid) -> _ErrorDescription:
def validate_entity_data(entity_data: dict) -> dict:
"""Validate entity data. Return validated data or raise EntityStoreValidationException."""
"""Validate entity data.
Return validated data or raise EntityStoreValidationException.
"""
try:
# return so defaults are applied
return ENTITY_STORE_DATA_SCHEMA(entity_data) # type: ignore[no-any-return]
+5 -8
View File
@@ -18,7 +18,6 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
@@ -38,7 +37,6 @@ from .const import (
from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .schema import SwitchSchema
from .storage.const import (
CONF_DEVICE_INFO,
CONF_ENTITY,
CONF_GA_PASSIVE,
CONF_GA_STATE,
@@ -133,14 +131,17 @@ class KnxYamlSwitch(_KnxSwitch, KnxYamlEntity):
class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
"""Representation of a KNX switch configured from UI."""
_attr_has_entity_name = True
_device: XknxSwitch
def __init__(
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
) -> None:
"""Initialize KNX switch."""
self._knx_module = knx_module
super().__init__(
knx_module=knx_module,
unique_id=unique_id,
entity_config=config[CONF_ENTITY],
)
self._device = XknxSwitch(
knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
@@ -153,7 +154,3 @@ class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
sync_state=config[DOMAIN][CONF_SYNC_STATE],
invert=config[DOMAIN][CONF_INVERT],
)
self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY]
self._attr_unique_id = unique_id
if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO):
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)})
@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["lmcloud"],
"requirements": ["lmcloud==1.2.2"]
"requirements": ["lmcloud==1.2.3"]
}
+1 -1
View File
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/lcn",
"iot_class": "local_push",
"loggers": ["pypck"],
"requirements": ["pypck==0.7.21", "lcn-frontend==0.1.6"]
"requirements": ["pypck==0.7.22", "lcn-frontend==0.1.6"]
}
+2 -2
View File
@@ -48,8 +48,8 @@
"iot_class": "local_polling",
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
"requirements": [
"aiolifx==1.0.9",
"aiolifx==1.1.1",
"aiolifx-effects==0.3.2",
"aiolifx-themes==0.5.0"
"aiolifx-themes==0.5.5"
]
}
@@ -20,6 +20,9 @@ from homeassistant.components.media_player import (
MediaType,
RepeatMode,
)
from homeassistant.components.media_player.browse_media import (
async_process_play_media_url,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
@@ -59,6 +62,7 @@ SOURCE_MAP: dict[PlayingMode, str] = {
PlayingMode.FM: "FM Radio",
PlayingMode.RCA: "RCA",
PlayingMode.UDISK: "USB",
PlayingMode.FOLLOWER: "Follower",
}
SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k for k, v in SOURCE_MAP.items()}
@@ -233,10 +237,14 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity):
self, media_type: MediaType | str, media_id: str, **kwargs: Any
) -> None:
"""Play a piece of media."""
media = await media_source.async_resolve_media(
self.hass, media_id, self.entity_id
)
await self._bridge.player.play(media.url)
if media_source.is_media_source_id(media_id):
play_item = await media_source.async_resolve_media(
self.hass, media_id, self.entity_id
)
media_id = play_item.url
url = async_process_play_media_url(self.hass, media_id)
await self._bridge.player.play(url)
def _update_properties(self) -> None:
"""Update the properties of the media player."""
+14 -17
View File
@@ -208,10 +208,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
if LYRIC_HVAC_MODE_COOL in device.allowed_modes:
self._attr_hvac_modes.append(HVACMode.COOL)
if (
LYRIC_HVAC_MODE_HEAT in device.allowed_modes
and LYRIC_HVAC_MODE_COOL in device.allowed_modes
):
if LYRIC_HVAC_MODE_HEAT_COOL in device.allowed_modes:
self._attr_hvac_modes.append(HVACMode.HEAT_COOL)
# Setup supported features
@@ -358,8 +355,8 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
await self._update_thermostat(
self.location,
device,
coolSetpoint=target_temp_high,
heatSetpoint=target_temp_low,
cool_setpoint=target_temp_high,
heat_setpoint=target_temp_low,
mode=mode,
)
except LYRIC_EXCEPTIONS as exception:
@@ -371,11 +368,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
try:
if self.hvac_mode == HVACMode.COOL:
await self._update_thermostat(
self.location, device, coolSetpoint=temp
self.location, device, cool_setpoint=temp
)
else:
await self._update_thermostat(
self.location, device, heatSetpoint=temp
self.location, device, heat_setpoint=temp
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
@@ -410,7 +407,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
self.location,
self.device,
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
autoChangeoverActive=False,
auto_changeover_active=False,
)
# Sleep 3 seconds before proceeding
await asyncio.sleep(3)
@@ -422,7 +419,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
self.location,
self.device,
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
autoChangeoverActive=True,
auto_changeover_active=True,
)
else:
_LOGGER.debug(
@@ -430,7 +427,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
HVAC_MODES[self.device.changeable_values.mode],
)
await self._update_thermostat(
self.location, self.device, autoChangeoverActive=True
self.location, self.device, auto_changeover_active=True
)
else:
_LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode])
@@ -438,13 +435,13 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
self.location,
self.device,
mode=LYRIC_HVAC_MODES[hvac_mode],
autoChangeoverActive=False,
auto_changeover_active=False,
)
async def _async_set_hvac_mode_lcc(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode for LCC devices (e.g., T5,6)."""
_LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode])
# Set autoChangeoverActive to True if the mode being passed is Auto
# Set auto_changeover_active to True if the mode being passed is Auto
# otherwise leave unchanged.
if (
LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL
@@ -458,7 +455,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
self.location,
self.device,
mode=LYRIC_HVAC_MODES[hvac_mode],
autoChangeoverActive=auto_changeover,
auto_changeover_active=auto_changeover,
)
async def async_set_preset_mode(self, preset_mode: str) -> None:
@@ -466,7 +463,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
_LOGGER.debug("Set preset mode: %s", preset_mode)
try:
await self._update_thermostat(
self.location, self.device, thermostatSetpointStatus=preset_mode
self.location, self.device, thermostat_setpoint_status=preset_mode
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
@@ -479,8 +476,8 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
await self._update_thermostat(
self.location,
self.device,
thermostatSetpointStatus=PRESET_HOLD_UNTIL,
nextPeriodTime=time_period,
thermostat_setpoint_status=PRESET_HOLD_UNTIL,
next_period_time=time_period,
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
+49 -41
View File
@@ -190,48 +190,56 @@ class MatterClimate(MatterEntity, ClimateEntity):
# if the mains power is off - treat it as if the HVAC mode is off
self._attr_hvac_mode = HVACMode.OFF
self._attr_hvac_action = None
return
# update hvac_mode from SystemMode
system_mode_value = int(
self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode)
)
match system_mode_value:
case SystemModeEnum.kAuto:
self._attr_hvac_mode = HVACMode.HEAT_COOL
case SystemModeEnum.kDry:
self._attr_hvac_mode = HVACMode.DRY
case SystemModeEnum.kFanOnly:
self._attr_hvac_mode = HVACMode.FAN_ONLY
case SystemModeEnum.kCool | SystemModeEnum.kPrecooling:
self._attr_hvac_mode = HVACMode.COOL
case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat:
self._attr_hvac_mode = HVACMode.HEAT
case SystemModeEnum.kFanOnly:
self._attr_hvac_mode = HVACMode.FAN_ONLY
case SystemModeEnum.kDry:
self._attr_hvac_mode = HVACMode.DRY
case _:
self._attr_hvac_mode = HVACMode.OFF
# running state is an optional attribute
# which we map to hvac_action if it exists (its value is not None)
self._attr_hvac_action = None
if running_state_value := self.get_matter_attribute_value(
clusters.Thermostat.Attributes.ThermostatRunningState
):
match running_state_value:
case ThermostatRunningState.Heat | ThermostatRunningState.HeatStage2:
self._attr_hvac_action = HVACAction.HEATING
case ThermostatRunningState.Cool | ThermostatRunningState.CoolStage2:
self._attr_hvac_action = HVACAction.COOLING
case (
ThermostatRunningState.Fan
| ThermostatRunningState.FanStage2
| ThermostatRunningState.FanStage3
):
self._attr_hvac_action = HVACAction.FAN
else:
# update hvac_mode from SystemMode
system_mode_value = int(
self.get_matter_attribute_value(
clusters.Thermostat.Attributes.SystemMode
)
)
match system_mode_value:
case SystemModeEnum.kAuto:
self._attr_hvac_mode = HVACMode.HEAT_COOL
case SystemModeEnum.kDry:
self._attr_hvac_mode = HVACMode.DRY
case SystemModeEnum.kFanOnly:
self._attr_hvac_mode = HVACMode.FAN_ONLY
case SystemModeEnum.kCool | SystemModeEnum.kPrecooling:
self._attr_hvac_mode = HVACMode.COOL
case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat:
self._attr_hvac_mode = HVACMode.HEAT
case SystemModeEnum.kFanOnly:
self._attr_hvac_mode = HVACMode.FAN_ONLY
case SystemModeEnum.kDry:
self._attr_hvac_mode = HVACMode.DRY
case _:
self._attr_hvac_action = HVACAction.OFF
self._attr_hvac_mode = HVACMode.OFF
# running state is an optional attribute
# which we map to hvac_action if it exists (its value is not None)
self._attr_hvac_action = None
if running_state_value := self.get_matter_attribute_value(
clusters.Thermostat.Attributes.ThermostatRunningState
):
match running_state_value:
case (
ThermostatRunningState.Heat
| ThermostatRunningState.HeatStage2
):
self._attr_hvac_action = HVACAction.HEATING
case (
ThermostatRunningState.Cool
| ThermostatRunningState.CoolStage2
):
self._attr_hvac_action = HVACAction.COOLING
case (
ThermostatRunningState.Fan
| ThermostatRunningState.FanStage2
| ThermostatRunningState.FanStage3
):
self._attr_hvac_action = HVACAction.FAN
case _:
self._attr_hvac_action = HVACAction.OFF
# update target temperature high/low
supports_range = (
self._attr_supported_features
+2 -2
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from aiomealie import MealieAuthenticationError, MealieClient, MealieConnectionError
from aiomealie import MealieAuthenticationError, MealieClient, MealieError
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL, Platform
from homeassistant.core import HomeAssistant
@@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo
version = create_version(about.version)
except MealieAuthenticationError as error:
raise ConfigEntryAuthFailed from error
except MealieConnectionError as error:
except MealieError as error:
raise ConfigEntryNotReady(error) from error
if not version.valid:
@@ -26,7 +26,13 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
for blind in motion_gateway.device_list.values():
if blind.limit_status == LimitStatus.Limit3Detected.name:
if blind.limit_status in (
LimitStatus.Limit3Detected.name,
{
"T": LimitStatus.Limit3Detected.name,
"B": LimitStatus.Limit3Detected.name,
},
):
entities.append(MotionGoFavoriteButton(coordinator, blind))
entities.append(MotionSetFavoriteButton(coordinator, blind))
@@ -21,5 +21,5 @@
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"iot_class": "local_push",
"loggers": ["motionblinds"],
"requirements": ["motionblinds==0.6.24"]
"requirements": ["motionblinds==0.6.25"]
}
+1 -1
View File
@@ -20,5 +20,5 @@
"iot_class": "cloud_push",
"loggers": ["google_nest_sdm"],
"quality_scale": "platinum",
"requirements": ["google-nest-sdm==5.0.0"]
"requirements": ["google-nest-sdm==5.0.1"]
}
+4 -2
View File
@@ -173,7 +173,9 @@ class NetatmoLight(NetatmoModuleEntity, LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn light on."""
if ATTR_BRIGHTNESS in kwargs:
await self.device.async_set_brightness(kwargs[ATTR_BRIGHTNESS])
await self.device.async_set_brightness(
round(kwargs[ATTR_BRIGHTNESS] / 2.55)
)
else:
await self.device.async_on()
@@ -194,6 +196,6 @@ class NetatmoLight(NetatmoModuleEntity, LightEntity):
if (brightness := self.device.brightness) is not None:
# Netatmo uses a range of [0, 100] to control brightness
self._attr_brightness = round((brightness / 100) * 255)
self._attr_brightness = round(brightness * 2.55)
else:
self._attr_brightness = None
+2 -3
View File
@@ -96,11 +96,10 @@ class PS4Device(MediaPlayerEntity):
self._retry = 0
self._disconnected = False
@callback
def status_callback(self) -> None:
"""Handle status callback. Parse status."""
self._parse_status()
self.async_write_ha_state()
self.schedule_update_ha_state()
@callback
def subscribe_to_protocol(self) -> None:
@@ -157,7 +156,7 @@ class PS4Device(MediaPlayerEntity):
self._ps4.ddp_protocol = self.hass.data[PS4_DATA].protocol
self.subscribe_to_protocol()
self._parse_status()
await self.hass.async_add_executor_job(self._parse_status)
def _parse_status(self) -> None:
"""Parse status."""
@@ -177,8 +177,12 @@ def count_torrents_in_states(
# When torrents are not in the returned data, there are none, return 0.
try:
torrents = cast(Mapping[str, Mapping], coordinator.data.get("torrents"))
if torrents is None:
return 0
if not states:
return len(torrents)
return len(
[torrent for torrent in torrents.values() if torrent.get("state") in states]
)
@@ -28,7 +28,7 @@ class RenaultBinarySensorEntityDescription(
"""Class describing Renault binary sensor entities."""
on_key: str
on_value: StateType
on_value: StateType | list[StateType]
async def async_setup_entry(
@@ -58,6 +58,9 @@ class RenaultBinarySensor(
"""Return true if the binary sensor is on."""
if (data := self._get_data_attr(self.entity_description.on_key)) is None:
return None
if isinstance(self.entity_description.on_value, list):
return data in self.entity_description.on_value
return data == self.entity_description.on_value
@@ -68,7 +71,10 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple(
coordinator="battery",
device_class=BinarySensorDeviceClass.PLUG,
on_key="plugStatus",
on_value=PlugState.PLUGGED.value,
on_value=[
PlugState.PLUGGED.value,
PlugState.PLUGGED_WAITING_FOR_CHARGE.value,
],
),
RenaultBinarySensorEntityDescription(
key="charging",
@@ -104,13 +110,13 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple(
]
+ [
RenaultBinarySensorEntityDescription(
key=f"{door.replace(' ','_').lower()}_door_status",
key=f"{door.replace(' ', '_').lower()}_door_status",
coordinator="lock_status",
# On means open, Off means closed
device_class=BinarySensorDeviceClass.DOOR,
on_key=f"doorStatus{door.replace(' ','')}",
on_key=f"doorStatus{door.replace(' ', '')}",
on_value="open",
translation_key=f"{door.lower().replace(' ','_')}_door_status",
translation_key=f"{door.lower().replace(' ', '_')}_door_status",
)
for door in ("Rear Left", "Rear Right", "Driver", "Passenger")
],
+7 -1
View File
@@ -197,7 +197,13 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = (
translation_key="plug_state",
device_class=SensorDeviceClass.ENUM,
entity_class=RenaultSensor[KamereonVehicleBatteryStatusData],
options=["unplugged", "plugged", "plug_error", "plug_unknown"],
options=[
"unplugged",
"plugged",
"plugged_waiting_for_charge",
"plug_error",
"plug_unknown",
],
value_lambda=_get_plug_state_formatted,
),
RenaultSensorEntityDescription(
@@ -141,6 +141,7 @@
"state": {
"unplugged": "Unplugged",
"plugged": "Plugged in",
"plugged_waiting_for_charge": "Plugged in, waiting for charge",
"plug_error": "Plug error",
"plug_unknown": "Plug unknown"
}
@@ -3,7 +3,7 @@
import asyncio
import logging
from aiorussound import Russound
from aiorussound import RussoundClient, RussoundTcpConnectionHandler
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
@@ -16,7 +16,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER]
_LOGGER = logging.getLogger(__name__)
type RussoundConfigEntry = ConfigEntry[Russound]
type RussoundConfigEntry = ConfigEntry[RussoundClient]
async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) -> bool:
@@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) ->
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
russ = Russound(hass.loop, host, port)
russ = RussoundClient(RussoundTcpConnectionHandler(hass.loop, host, port))
@callback
def is_connected_updated(connected: bool) -> None:
@@ -37,14 +37,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) ->
port,
)
russ.add_connection_callback(is_connected_updated)
russ.connection_handler.add_connection_callback(is_connected_updated)
try:
async with asyncio.timeout(CONNECT_TIMEOUT):
await russ.connect()
except RUSSOUND_RIO_EXCEPTIONS as err:
raise ConfigEntryNotReady(f"Error while connecting to {host}:{port}") from err
entry.runtime_data = russ
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -6,7 +6,7 @@ import asyncio
import logging
from typing import Any
from aiorussound import Controller, Russound
from aiorussound import Controller, RussoundClient, RussoundTcpConnectionHandler
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -54,8 +54,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
host = user_input[CONF_HOST]
port = user_input[CONF_PORT]
controllers = None
russ = Russound(self.hass.loop, host, port)
russ = RussoundClient(
RussoundTcpConnectionHandler(self.hass.loop, host, port)
)
try:
async with asyncio.timeout(CONNECT_TIMEOUT):
await russ.connect()
@@ -87,7 +88,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
port = import_data.get(CONF_PORT, 9621)
# Connection logic is repeated here since this method will be removed in future releases
russ = Russound(self.hass.loop, host, port)
russ = RussoundClient(RussoundTcpConnectionHandler(self.hass.loop, host, port))
try:
async with asyncio.timeout(CONNECT_TIMEOUT):
await russ.connect()
@@ -2,7 +2,7 @@
import asyncio
from aiorussound import CommandException
from aiorussound import CommandError
from aiorussound.const import FeatureFlag
from homeassistant.components.media_player import MediaPlayerEntityFeature
@@ -10,7 +10,7 @@ from homeassistant.components.media_player import MediaPlayerEntityFeature
DOMAIN = "russound_rio"
RUSSOUND_RIO_EXCEPTIONS = (
CommandException,
CommandError,
ConnectionRefusedError,
TimeoutError,
asyncio.CancelledError,
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Concatenate
from aiorussound import Controller
from aiorussound import Controller, RussoundTcpConnectionHandler
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
@@ -53,7 +53,6 @@ class RussoundBaseEntity(Entity):
or f"{self._primary_mac_address}-{self._controller.controller_id}"
)
self._attr_device_info = DeviceInfo(
configuration_url=f"http://{self._instance.host}",
# Use MAC address of Russound device as identifier
identifiers={(DOMAIN, self._device_identifier)},
manufacturer="Russound",
@@ -61,6 +60,10 @@ class RussoundBaseEntity(Entity):
model=controller.controller_type,
sw_version=controller.firmware_version,
)
if isinstance(self._instance.connection_handler, RussoundTcpConnectionHandler):
self._attr_device_info["configuration_url"] = (
f"http://{self._instance.connection_handler.host}"
)
if controller.parent_controller:
self._attr_device_info["via_device"] = (
DOMAIN,
@@ -79,8 +82,12 @@ class RussoundBaseEntity(Entity):
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
self._instance.add_connection_callback(self._is_connected_updated)
self._instance.connection_handler.add_connection_callback(
self._is_connected_updated
)
async def async_will_remove_from_hass(self) -> None:
"""Remove callbacks."""
self._instance.remove_connection_callback(self._is_connected_updated)
self._instance.connection_handler.remove_connection_callback(
self._is_connected_updated
)
@@ -7,5 +7,5 @@
"iot_class": "local_push",
"loggers": ["aiorussound"],
"quality_scale": "silver",
"requirements": ["aiorussound==2.3.2"]
"requirements": ["aiorussound==3.0.5"]
}
@@ -84,14 +84,16 @@ async def async_setup_entry(
"""Set up the Russound RIO platform."""
russ = entry.runtime_data
await russ.init_sources()
sources = russ.sources
for source in sources.values():
await source.watch()
# Discover controllers
controllers = await russ.enumerate_controllers()
entities = []
for controller in controllers.values():
sources = controller.sources
for source in sources.values():
await source.watch()
for zone in controller.zones.values():
await zone.watch()
mp = RussoundZoneDevice(zone, sources)
@@ -154,7 +156,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
@property
def state(self) -> MediaPlayerState | None:
"""Return the state of the device."""
status = self._zone.status
status = self._zone.properties.status
if status == "ON":
return MediaPlayerState.ON
if status == "OFF":
@@ -174,22 +176,22 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
@property
def media_title(self):
"""Title of current playing media."""
return self._current_source().song_name
return self._current_source().properties.song_name
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
return self._current_source().artist_name
return self._current_source().properties.artist_name
@property
def media_album_name(self):
"""Album name of current playing media, music track only."""
return self._current_source().album_name
return self._current_source().properties.album_name
@property
def media_image_url(self):
"""Image url of current playing media."""
return self._current_source().cover_art_url
return self._current_source().properties.cover_art_url
@property
def volume_level(self):
@@ -198,7 +200,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
Value is returned based on a range (0..50).
Therefore float divide by 50 to get to the required range.
"""
return float(self._zone.volume or "0") / 50.0
return float(self._zone.properties.volume or "0") / 50.0
@command
async def async_turn_off(self) -> None:
@@ -214,7 +216,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
async def async_set_volume_level(self, volume: float) -> None:
"""Set the volume level."""
rvol = int(volume * 50.0)
await self._zone.set_volume(rvol)
await self._zone.set_volume(str(rvol))
@command
async def async_select_source(self, source: str) -> None:
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/russound_rnet",
"iot_class": "local_polling",
"loggers": ["russound"],
"requirements": ["russound==0.1.9"]
"requirements": ["russound==0.2.0"]
}
@@ -96,7 +96,13 @@ class RussoundRNETDevice(MediaPlayerEntity):
# Updated this function to make a single call to get_zone_info, so that
# with a single call we can get On/Off, Volume and Source, reducing the
# amount of traffic and speeding up the update process.
ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4)
try:
ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4)
except BrokenPipeError:
_LOGGER.error("Broken Pipe Error, trying to reconnect to Russound RNET")
self._russ.connect()
ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4)
_LOGGER.debug("ret= %s", ret)
if ret is not None:
_LOGGER.debug(
+1 -2
View File
@@ -42,5 +42,4 @@ class SchlageEntity(CoordinatorEntity[SchlageDataUpdateCoordinator]):
@property
def available(self) -> bool:
"""Return if entity is available."""
# When is_locked is None the lock is unavailable.
return super().available and self._lock.is_locked is not None
return super().available and self.device_id in self.coordinator.data.locks
+3 -2
View File
@@ -42,8 +42,9 @@ class SchlageLockEntity(SchlageEntity, LockEntity):
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_attrs()
return super()._handle_coordinator_update()
if self.device_id in self.coordinator.data.locks:
self._update_attrs()
super()._handle_coordinator_update()
def _update_attrs(self) -> None:
"""Update our internal state attributes."""
+3 -2
View File
@@ -64,5 +64,6 @@ class SchlageBatterySensor(SchlageEntity, SensorEntity):
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._attr_native_value = getattr(self._lock, self.entity_description.key)
return super()._handle_coordinator_update()
if self.device_id in self.coordinator.data.locks:
self._attr_native_value = getattr(self._lock, self.entity_description.key)
super()._handle_coordinator_update()
@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING
from sfrbox_api.bridge import SFRBox
from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError
@@ -46,6 +47,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Preload system information
await data.system.async_config_entry_first_refresh()
system_info = data.system.data
if TYPE_CHECKING:
assert system_info is not None
# Preload other coordinators (based on net infrastructure)
tasks = [data.wan.async_config_entry_first_refresh()]
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo
@@ -65,19 +66,22 @@ async def async_setup_entry(
) -> None:
"""Set up the sensors."""
data: DomainData = hass.data[DOMAIN][entry.entry_id]
system_info = data.system.data
if TYPE_CHECKING:
assert system_info is not None
entities: list[SFRBoxBinarySensor] = [
SFRBoxBinarySensor(data.wan, description, data.system.data)
SFRBoxBinarySensor(data.wan, description, system_info)
for description in WAN_SENSOR_TYPES
]
if (net_infra := data.system.data.net_infra) == "adsl":
if (net_infra := system_info.net_infra) == "adsl":
entities.extend(
SFRBoxBinarySensor(data.dsl, description, data.system.data)
SFRBoxBinarySensor(data.dsl, description, system_info)
for description in DSL_SENSOR_TYPES
)
elif net_infra == "ftth":
entities.extend(
SFRBoxBinarySensor(data.ftth, description, data.system.data)
SFRBoxBinarySensor(data.ftth, description, system_info)
for description in FTTH_SENSOR_TYPES
)
@@ -111,4 +115,6 @@ class SFRBoxBinarySensor[_T](
@property
def is_on(self) -> bool | None:
"""Return the native value of the device."""
if self.coordinator.data is None:
return None
return self.entity_description.value_fn(self.coordinator.data)
+5 -3
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from dataclasses import dataclass
from functools import wraps
from typing import Any, Concatenate
from typing import TYPE_CHECKING, Any, Concatenate
from sfrbox_api.bridge import SFRBox
from sfrbox_api.exceptions import SFRBoxError
@@ -69,10 +69,12 @@ async def async_setup_entry(
) -> None:
"""Set up the buttons."""
data: DomainData = hass.data[DOMAIN][entry.entry_id]
system_info = data.system.data
if TYPE_CHECKING:
assert system_info is not None
entities = [
SFRBoxButton(data.box, description, data.system.data)
for description in BUTTON_TYPES
SFRBoxButton(data.box, description, system_info) for description in BUTTON_TYPES
]
async_add_entities(entities)
@@ -3,7 +3,7 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from typing import TYPE_CHECKING, Any
from sfrbox_api.bridge import SFRBox
from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError
@@ -51,6 +51,8 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN):
except SFRBoxError:
errors["base"] = "cannot_connect"
else:
if TYPE_CHECKING:
assert system_info is not None
await self.async_set_unique_id(system_info.mac_addr)
self._abort_if_unique_id_configured()
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
@@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
_SCAN_INTERVAL = timedelta(minutes=1)
class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT | None]):
"""Coordinator to manage data updates."""
def __init__(
@@ -23,14 +23,14 @@ class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
hass: HomeAssistant,
box: SFRBox,
name: str,
method: Callable[[SFRBox], Coroutine[Any, Any, _DataT]],
method: Callable[[SFRBox], Coroutine[Any, Any, _DataT | None]],
) -> None:
"""Initialize coordinator."""
self.box = box
self._method = method
super().__init__(hass, _LOGGER, name=name, update_interval=_SCAN_INTERVAL)
async def _async_update_data(self) -> _DataT:
async def _async_update_data(self) -> _DataT | None:
"""Update data."""
try:
return await self._method(self.box)
+14 -17
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
import dataclasses
from typing import Any
from typing import TYPE_CHECKING, Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
@@ -12,9 +12,18 @@ from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .models import DomainData
if TYPE_CHECKING:
from _typeshed import DataclassInstance
TO_REDACT = {"mac_addr", "serial_number", "ip_addr", "ipv6_addr"}
def _async_redact_data(obj: DataclassInstance | None) -> dict[str, Any] | None:
if obj is None:
return None
return async_redact_data(dataclasses.asdict(obj), TO_REDACT)
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
@@ -27,21 +36,9 @@ async def async_get_config_entry_diagnostics(
"data": dict(entry.data),
},
"data": {
"dsl": async_redact_data(
dataclasses.asdict(await data.system.box.dsl_get_info()),
TO_REDACT,
),
"ftth": async_redact_data(
dataclasses.asdict(await data.system.box.ftth_get_info()),
TO_REDACT,
),
"system": async_redact_data(
dataclasses.asdict(await data.system.box.system_get_info()),
TO_REDACT,
),
"wan": async_redact_data(
dataclasses.asdict(await data.system.box.wan_get_info()),
TO_REDACT,
),
"dsl": _async_redact_data(await data.system.box.dsl_get_info()),
"ftth": _async_redact_data(await data.system.box.ftth_get_info()),
"system": _async_redact_data(await data.system.box.system_get_info()),
"wan": _async_redact_data(await data.system.box.wan_get_info()),
},
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/sfr_box",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["sfrbox-api==0.0.8"]
"requirements": ["sfrbox-api==0.0.11"]
}
+25 -7
View File
@@ -2,6 +2,7 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from sfrbox_api.models import DslInfo, SystemInfo, WanInfo
@@ -129,7 +130,7 @@ DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = (
"unknown",
],
translation_key="dsl_line_status",
value_fn=lambda x: x.line_status.lower().replace(" ", "_"),
value_fn=lambda x: _value_to_option(x.line_status),
),
SFRBoxSensorEntityDescription[DslInfo](
key="training",
@@ -149,7 +150,7 @@ DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = (
"unknown",
],
translation_key="dsl_training",
value_fn=lambda x: x.training.lower().replace(" ", "_").replace(".", "_"),
value_fn=lambda x: _value_to_option(x.training),
),
)
SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = (
@@ -181,7 +182,7 @@ SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda x: None if x.temperature is None else x.temperature / 1000,
value_fn=lambda x: _get_temperature(x.temperature),
),
)
WAN_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[WanInfo], ...] = (
@@ -203,23 +204,38 @@ WAN_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[WanInfo], ...] = (
)
def _value_to_option(value: str | None) -> str | None:
if value is None:
return value
return value.lower().replace(" ", "_").replace(".", "_")
def _get_temperature(value: float | None) -> float | None:
if value is None or value < 1000:
return value
return value / 1000
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the sensors."""
data: DomainData = hass.data[DOMAIN][entry.entry_id]
system_info = data.system.data
if TYPE_CHECKING:
assert system_info is not None
entities: list[SFRBoxSensor] = [
SFRBoxSensor(data.system, description, data.system.data)
SFRBoxSensor(data.system, description, system_info)
for description in SYSTEM_SENSOR_TYPES
]
entities.extend(
SFRBoxSensor(data.wan, description, data.system.data)
SFRBoxSensor(data.wan, description, system_info)
for description in WAN_SENSOR_TYPES
)
if data.system.data.net_infra == "adsl":
if system_info.net_infra == "adsl":
entities.extend(
SFRBoxSensor(data.dsl, description, data.system.data)
SFRBoxSensor(data.dsl, description, system_info)
for description in DSL_SENSOR_TYPES
)
@@ -251,4 +267,6 @@ class SFRBoxSensor[_T](CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SensorEn
@property
def native_value(self) -> StateType:
"""Return the native value of the device."""
if self.coordinator.data is None:
return None
return self.entity_description.value_fn(self.coordinator.data)
@@ -94,8 +94,13 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
mac = discovery_info.properties.get("mac")
# fallback for legacy firmware
if mac is None:
info = await self.client.get_info()
try:
info = await self.client.get_info()
except SmlightConnectionError:
# User is likely running unsupported ESPHome firmware
return self.async_abort(reason="cannot_connect")
mac = info.MAC
await self.async_set_unique_id(format_mac(mac))
self._abort_if_unique_id_configured()
@@ -9,8 +9,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@@ -40,6 +42,7 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]):
self.unique_id: str | None = None
self.client = Api2(host=host, session=async_get_clientsession(hass))
self.legacy_api: int = 0
async def _async_setup(self) -> None:
"""Authenticate if needed during initial setup."""
@@ -60,11 +63,28 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]):
info = await self.client.get_info()
self.unique_id = format_mac(info.MAC)
if info.legacy_api:
self.legacy_api = info.legacy_api
ir.async_create_issue(
self.hass,
DOMAIN,
"unsupported_firmware",
is_fixable=False,
is_persistent=False,
learn_more_url="https://smlight.tech/flasher/#SLZB-06",
severity=IssueSeverity.ERROR,
translation_key="unsupported_firmware",
)
async def _async_update_data(self) -> SmData:
"""Fetch data from the SMLIGHT device."""
try:
sensors = Sensors()
if not self.legacy_api:
sensors = await self.client.get_sensors()
return SmData(
sensors=await self.client.get_sensors(),
sensors=sensors,
info=await self.client.get_info(),
)
except SmlightConnectionError as err:
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/smlight",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["pysmlight==0.0.13"],
"requirements": ["pysmlight==0.0.14"],
"zeroconf": [
{
"type": "_slzb-06._tcp.local."
@@ -45,5 +45,11 @@
"name": "RAM usage"
}
}
},
"issues": {
"unsupported_firmware": {
"title": "SLZB core firmware update required",
"description": "Your SMLIGHT SLZB-06x device is running an unsupported core firmware version. Please update it to the latest version to enjoy all the features of this integration."
}
}
}
+19 -5
View File
@@ -84,6 +84,7 @@ REPEAT_TO_SONOS = {
SONOS_TO_REPEAT = {meaning: mode for mode, meaning in REPEAT_TO_SONOS.items()}
UPNP_ERRORS_TO_IGNORE = ["701", "711", "712"]
ANNOUNCE_NOT_SUPPORTED_ERRORS: list[str] = ["globalError"]
SERVICE_SNAPSHOT = "snapshot"
SERVICE_RESTORE = "restore"
@@ -556,11 +557,24 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
) from exc
if response.get("success"):
return
raise HomeAssistantError(
translation_domain=SONOS_DOMAIN,
translation_key="announce_media_error",
translation_placeholders={"media_id": media_id, "response": response},
)
if response.get("type") in ANNOUNCE_NOT_SUPPORTED_ERRORS:
# If the speaker does not support announce do not raise and
# fall through to_play_media to play the clip directly.
_LOGGER.debug(
"Speaker %s does not support announce, media_id %s response %s",
self.speaker.zone_name,
media_id,
response,
)
else:
raise HomeAssistantError(
translation_domain=SONOS_DOMAIN,
translation_key="announce_media_error",
translation_placeholders={
"media_id": media_id,
"response": response,
},
)
if spotify.is_spotify_media_type(media_type):
media_type = spotify.resolve_spotify_media_type(media_type)
@@ -10,9 +10,8 @@ import surepy
from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, SURE_API_TIMEOUT
@@ -27,57 +26,43 @@ USER_DATA_SCHEMA = vol.Schema(
)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect."""
surepy_client = surepy.Surepy(
data[CONF_USERNAME],
data[CONF_PASSWORD],
auth_token=None,
api_timeout=SURE_API_TIMEOUT,
session=async_get_clientsession(hass),
)
token = await surepy_client.sac.get_token()
return {CONF_TOKEN: token}
class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Sure Petcare."""
VERSION = 1
def __init__(self) -> None:
"""Initialize."""
self._username: str | None = None
reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(step_id="user", data_schema=USER_DATA_SCHEMA)
errors = {}
try:
info = await validate_input(self.hass, user_input)
except SurePetcareAuthenticationError:
errors["base"] = "invalid_auth"
except SurePetcareError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
self._abort_if_unique_id_configured()
user_input[CONF_TOKEN] = info[CONF_TOKEN]
return self.async_create_entry(
title="Sure Petcare",
data=user_input,
if user_input is not None:
client = surepy.Surepy(
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
auth_token=None,
api_timeout=SURE_API_TIMEOUT,
session=async_get_clientsession(self.hass),
)
try:
token = await client.sac.get_token()
except SurePetcareAuthenticationError:
errors["base"] = "invalid_auth"
except SurePetcareError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="Sure Petcare",
data={**user_input, CONF_TOKEN: token},
)
return self.async_show_form(
step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors
@@ -87,18 +72,27 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
self._username = entry_data[CONF_USERNAME]
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
assert self.reauth_entry
errors = {}
if user_input is not None:
user_input[CONF_USERNAME] = self._username
client = surepy.Surepy(
self.reauth_entry.data[CONF_USERNAME],
user_input[CONF_PASSWORD],
auth_token=None,
api_timeout=SURE_API_TIMEOUT,
session=async_get_clientsession(self.hass),
)
try:
await validate_input(self.hass, user_input)
token = await client.sac.get_token()
except SurePetcareAuthenticationError:
errors["base"] = "invalid_auth"
except SurePetcareError:
@@ -107,16 +101,20 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
existing_entry = await self.async_set_unique_id(
user_input[CONF_USERNAME].lower()
return self.async_update_reload_and_abort(
self.reauth_entry,
data={
**self.reauth_entry.data,
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_TOKEN: token,
},
)
if existing_entry:
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={"username": self._username},
description_placeholders={
"username": self.reauth_entry.data[CONF_USERNAME]
},
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
errors=errors,
)
+17 -3
View File
@@ -7,9 +7,14 @@ import logging
from homeassistant import config as conf_util
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID, SERVICE_RELOAD
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_NAME,
CONF_UNIQUE_ID,
SERVICE_RELOAD,
)
from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import ConfigEntryError, HomeAssistantError
from homeassistant.helpers import discovery
from homeassistant.helpers.device import (
async_remove_stale_devices_links_keep_current_device,
@@ -19,7 +24,7 @@ from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration
from .const import CONF_TRIGGER, DOMAIN, PLATFORMS
from .const import CONF_MAX, CONF_MIN, CONF_STEP, CONF_TRIGGER, DOMAIN, PLATFORMS
from .coordinator import TriggerUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -67,6 +72,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.options.get(CONF_DEVICE_ID),
)
for key in (CONF_MAX, CONF_MIN, CONF_STEP):
if key not in entry.options:
continue
if isinstance(entry.options[key], str):
raise ConfigEntryError(
f"The '{entry.options.get(CONF_NAME) or ""}' number template needs to "
f"be reconfigured, {key} must be a number, got '{entry.options[key]}'"
)
await hass.config_entries.async_forward_entry_setups(
entry, (entry.options["template_type"],)
)
@@ -107,15 +107,15 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
if domain == Platform.NUMBER:
schema |= {
vol.Required(CONF_STATE): selector.TemplateSelector(),
vol.Required(
CONF_MIN, default=f"{{{{{DEFAULT_MIN_VALUE}}}}}"
): selector.TemplateSelector(),
vol.Required(
CONF_MAX, default=f"{{{{{DEFAULT_MAX_VALUE}}}}}"
): selector.TemplateSelector(),
vol.Required(
CONF_STEP, default=f"{{{{{DEFAULT_STEP}}}}}"
): selector.TemplateSelector(),
vol.Required(CONF_MIN, default=DEFAULT_MIN_VALUE): selector.NumberSelector(
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
),
vol.Required(CONF_MAX, default=DEFAULT_MAX_VALUE): selector.NumberSelector(
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
),
vol.Required(CONF_STEP, default=DEFAULT_STEP): selector.NumberSelector(
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
),
vol.Optional(CONF_SET_VALUE): selector.ActionSelector(),
}
+6 -3
View File
@@ -28,11 +28,14 @@ PLATFORMS = [
Platform.WEATHER,
]
CONF_AVAILABILITY = "availability"
CONF_ATTRIBUTES = "attributes"
CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
CONF_ATTRIBUTES = "attributes"
CONF_AVAILABILITY = "availability"
CONF_MAX = "max"
CONF_MIN = "min"
CONF_OBJECT_ID = "object_id"
CONF_PICTURE = "picture"
CONF_PRESS = "press"
CONF_OBJECT_ID = "object_id"
CONF_STEP = "step"
CONF_TURN_OFF = "turn_off"
CONF_TURN_ON = "turn_on"
+1 -4
View File
@@ -31,7 +31,7 @@ from homeassistant.helpers.script import Script
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import TriggerUpdateCoordinator
from .const import DOMAIN
from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN
from .template_entity import (
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_ICON_SCHEMA,
@@ -42,9 +42,6 @@ from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__)
CONF_SET_VALUE = "set_value"
CONF_MIN = "min"
CONF_MAX = "max"
CONF_STEP = "step"
DEFAULT_NAME = "Template Number"
DEFAULT_OPTIMISTIC = False
@@ -192,3 +192,10 @@ class TeslemetryWallConnectorEntity(
.get(self.din, {})
.get(self.key)
)
@property
def exists(self) -> bool:
"""Return True if it exists in the wall connector coordinator data."""
return self.key in self.coordinator.data.get("wall_connectors", {}).get(
self.din, {}
)
+10 -9
View File
@@ -379,18 +379,18 @@ ENERGY_LIVE_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(key="island_status", device_class=SensorDeviceClass.ENUM),
)
WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
WALL_CONNECTOR_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = (
TeslemetrySensorEntityDescription(
key="wall_connector_state",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
TeslemetrySensorEntityDescription(
key="wall_connector_fault_state",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
TeslemetrySensorEntityDescription(
key="wall_connector_power",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
@@ -398,8 +398,9 @@ WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
suggested_display_precision=2,
device_class=SensorDeviceClass.POWER,
),
SensorEntityDescription(
TeslemetrySensorEntityDescription(
key="vin",
value_fn=lambda vin: vin or "disconnected",
),
)
@@ -525,13 +526,13 @@ class TeslemetryEnergyLiveSensorEntity(TeslemetryEnergyLiveEntity, SensorEntity)
class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorEntity):
"""Base class for Teslemetry energy site metric sensors."""
entity_description: SensorEntityDescription
entity_description: TeslemetrySensorEntityDescription
def __init__(
self,
data: TeslemetryEnergyData,
din: str,
description: SensorEntityDescription,
description: TeslemetrySensorEntityDescription,
) -> None:
"""Initialize the sensor."""
self.entity_description = description
@@ -543,8 +544,8 @@ class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorE
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_available = not self.is_none
self._attr_native_value = self._value
if self.exists:
self._attr_native_value = self.entity_description.value_fn(self._value)
class TeslemetryEnergyInfoSensorEntity(TeslemetryEnergyInfoEntity, SensorEntity):
@@ -420,7 +420,10 @@
"name": "version"
},
"vin": {
"name": "Vehicle"
"name": "Vehicle",
"state": {
"disconnected": "Disconnected"
}
},
"vpp_backup_reserve_percent": {
"name": "VPP backup reserve"
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["tibber"],
"quality_scale": "silver",
"requirements": ["pyTibber==0.30.1"]
"requirements": ["pyTibber==0.30.2"]
}
+83 -33
View File
@@ -26,6 +26,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_ALIAS,
CONF_AUTHENTICATION,
CONF_DEVICE,
CONF_HOST,
CONF_MAC,
CONF_MODEL,
@@ -44,8 +45,12 @@ from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_AES_KEYS,
CONF_CONFIG_ENTRY_MINOR_VERSION,
CONF_CONNECTION_PARAMETERS,
CONF_CREDENTIALS_HASH,
CONF_DEVICE_CONFIG,
CONF_USES_HTTP,
CONNECT_TIMEOUT,
DISCOVERY_TIMEOUT,
DOMAIN,
@@ -85,9 +90,7 @@ def async_trigger_discovery(
CONF_ALIAS: device.alias or mac_alias(device.mac),
CONF_HOST: device.host,
CONF_MAC: formatted_mac,
CONF_DEVICE_CONFIG: device.config.to_dict(
exclude_credentials=True,
),
CONF_DEVICE: device,
},
)
@@ -136,25 +139,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
host: str = entry.data[CONF_HOST]
credentials = await get_credentials(hass)
entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH)
entry_use_http = entry.data.get(CONF_USES_HTTP, False)
entry_aes_keys = entry.data.get(CONF_AES_KEYS)
config: DeviceConfig | None = None
if config_dict := entry.data.get(CONF_DEVICE_CONFIG):
conn_params: Device.ConnectionParameters | None = None
if conn_params_dict := entry.data.get(CONF_CONNECTION_PARAMETERS):
try:
config = DeviceConfig.from_dict(config_dict)
conn_params = Device.ConnectionParameters.from_dict(conn_params_dict)
except KasaException:
_LOGGER.warning(
"Invalid connection type dict for %s: %s", host, config_dict
"Invalid connection parameters dict for %s: %s", host, conn_params_dict
)
if not config:
config = DeviceConfig(host)
else:
config.host = host
config.timeout = CONNECT_TIMEOUT
if config.uses_http is True:
config.http_client = create_async_tplink_clientsession(hass)
client = create_async_tplink_clientsession(hass) if entry_use_http else None
config = DeviceConfig(
host,
timeout=CONNECT_TIMEOUT,
http_client=client,
aes_keys=entry_aes_keys,
)
if conn_params:
config.connection_type = conn_params
# If we have in memory credentials use them otherwise check for credentials_hash
if credentials:
config.credentials = credentials
@@ -173,14 +178,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
raise ConfigEntryNotReady from ex
device_credentials_hash = device.credentials_hash
device_config_dict = device.config.to_dict(exclude_credentials=True)
# Do not store the credentials hash inside the device_config
device_config_dict.pop(CONF_CREDENTIALS_HASH, None)
# We not need to update the connection parameters or the use_http here
# because if they were wrong we would have failed to connect.
# Discovery will update those if necessary.
updates: dict[str, Any] = {}
if device_credentials_hash and device_credentials_hash != entry_credentials_hash:
updates[CONF_CREDENTIALS_HASH] = device_credentials_hash
if device_config_dict != config_dict:
updates[CONF_DEVICE_CONFIG] = device_config_dict
if entry_aes_keys != device.config.aes_keys:
updates[CONF_AES_KEYS] = device.config.aes_keys
if entry.data.get(CONF_ALIAS) != device.alias:
updates[CONF_ALIAS] = device.alias
if entry.data.get(CONF_MODEL) != device.model:
@@ -307,12 +313,20 @@ def _device_id_is_mac_or_none(mac: str, device_ids: Iterable[str]) -> str | None
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
version = config_entry.version
minor_version = config_entry.minor_version
entry_version = config_entry.version
entry_minor_version = config_entry.minor_version
# having a condition to check for the current version allows
# tests to be written per migration step.
config_flow_minor_version = CONF_CONFIG_ENTRY_MINOR_VERSION
_LOGGER.debug("Migrating from version %s.%s", version, minor_version)
if version == 1 and minor_version < 3:
new_minor_version = 3
if (
entry_version == 1
and entry_minor_version < new_minor_version <= config_flow_minor_version
):
_LOGGER.debug(
"Migrating from version %s.%s", entry_version, entry_minor_version
)
# Previously entities on child devices added themselves to the parent
# device and set their device id as identifiers along with mac
# as a connection which creates a single device entry linked by all
@@ -359,12 +373,19 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
new_identifiers,
)
minor_version = 3
hass.config_entries.async_update_entry(config_entry, minor_version=3)
hass.config_entries.async_update_entry(
config_entry, minor_version=new_minor_version
)
_LOGGER.debug("Migration to version %s.%s complete", version, minor_version)
_LOGGER.debug(
"Migration to version %s.%s complete", entry_version, new_minor_version
)
if version == 1 and minor_version == 3:
new_minor_version = 4
if (
entry_version == 1
and entry_minor_version < new_minor_version <= config_flow_minor_version
):
# credentials_hash stored in the device_config should be moved to data.
updates: dict[str, Any] = {}
if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG):
@@ -372,15 +393,44 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH, None):
updates[CONF_CREDENTIALS_HASH] = credentials_hash
updates[CONF_DEVICE_CONFIG] = config_dict
minor_version = 4
hass.config_entries.async_update_entry(
config_entry,
data={
**config_entry.data,
**updates,
},
minor_version=minor_version,
minor_version=new_minor_version,
)
_LOGGER.debug(
"Migration to version %s.%s complete", entry_version, new_minor_version
)
_LOGGER.debug("Migration to version %s.%s complete", version, minor_version)
new_minor_version = 5
if (
entry_version == 1
and entry_minor_version < new_minor_version <= config_flow_minor_version
):
# complete device config no longer to be stored, only required
# attributes like connection parameters and aes_keys
updates = {}
entry_data = {
k: v for k, v in config_entry.data.items() if k != CONF_DEVICE_CONFIG
}
if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG):
assert isinstance(config_dict, dict)
if connection_parameters := config_dict.get("connection_type"):
updates[CONF_CONNECTION_PARAMETERS] = connection_parameters
if (use_http := config_dict.get(CONF_USES_HTTP)) is not None:
updates[CONF_USES_HTTP] = use_http
hass.config_entries.async_update_entry(
config_entry,
data={
**entry_data,
**updates,
},
minor_version=new_minor_version,
)
_LOGGER.debug(
"Migration to version %s.%s complete", entry_version, new_minor_version
)
return True

Some files were not shown because too many files have changed in this diff Show More