Compare commits

..

103 Commits

Author SHA1 Message Date
Franck Nijhof
0b91a92554 Bump version to 2025.11.0 2025-11-05 19:22:08 +00:00
Franck Nijhof
7855df92c8 2025.11 (#155440) 2025-11-05 11:20:38 -08:00
Franck Nijhof
11309f89f0 Bump version to 2025.11.0b6 2025-11-05 18:38:57 +00:00
Paulus Schoutsen
396a987035 Rename DALI Center to Sunricher DALI (#155865)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-11-05 18:29:29 +00:00
puddly
b7696bfb20 Allow hardware integrations to specify TX power for ZHA (#155855)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-11-05 18:29:27 +00:00
Bram Kragten
5cfbe2cf71 Update frontend to 20251105.0 (#155853) 2025-11-05 18:29:26 +00:00
Erik Montnemery
4e255286af Create issue to warn against using http.server_host in supervised installs (#155837) 2025-11-05 18:29:25 +00:00
Franck Nijhof
53a96af844 Bump version to 2025.11.0b5 2025-11-05 10:38:26 +00:00
Erik Montnemery
accb705d8b Fix ESPHome config entry unload (#155830) 2025-11-05 10:29:40 +00:00
Foscam-wangzhengyu
1793abce4f Bump libpyfoscamcgi to 0.0.9 (#155824) 2025-11-05 10:29:38 +00:00
Nathan Spencer
8bfed0b60c Bump pylitterbot to 2025.0.0 (#155821) 2025-11-05 10:29:37 +00:00
steinmn
016c1de2ef Set LG Thinq energy sensor state_class as total_increasing (#155816) 2025-11-05 10:29:35 +00:00
G Johansson
c270f31365 Bump holidays to 0.84 (#155802) 2025-11-05 10:29:34 +00:00
puddly
f9e06acfc7 Add progress to ZHA migration steps (#155764)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-11-05 10:29:32 +00:00
Bouwe Westerdijk
901558b293 Bugfix: implement RestoreState and bump backend for Plugwise climate (#155126) 2025-11-05 10:29:31 +00:00
Aarni Koskela
c09cf36345 Bump ruuvitag-ble to 0.3.0 (#155720) 2025-11-05 11:14:31 +01:00
Bram Kragten
926627b49c Bump version to 2025.11.0b4 2025-11-04 20:38:30 +01:00
Bram Kragten
a8eeba9c5f Update frontend to 20251104.0 (#155799) 2025-11-04 20:38:02 +01:00
Paul Bottein
e4591c27c0 Rename safety panel to security panel (#155795) 2025-11-04 20:38:01 +01:00
starkillerOG
40dedec602 Bump reolink-aio to 0.16.4 (#155776) 2025-11-04 20:38:00 +01:00
Matt Zimmerman
3a65b5ca70 Update python-smarttub to 0.0.45 (#155768) 2025-11-04 20:37:59 +01:00
puddly
dbeb82861f Bump ZHA to 0.0.77 (#155766) 2025-11-04 20:37:58 +01:00
Brett Adams
e43c35ab2d Bump Tesla Fleet API to v1.2.5 (#155763) 2025-11-04 20:37:57 +01:00
karwosts
b4c4fdefe3 Fix Ambient Weather incorrect state classes (#155751) 2025-11-04 20:37:56 +01:00
Fredrik Mårtensson
965dd7c557 Catch exception from libsoundtouch if device not available (#155749)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-11-04 20:37:55 +01:00
TheJulianJES
9a921f2c8e Fix ZBT-2 Thread to Zigbee migration discovery failing (#155735) 2025-11-04 20:37:54 +01:00
cdnninja
aaae3244a8 Correct Vesync Humidifier Mode (#155638) 2025-11-04 20:37:53 +01:00
TheJulianJES
40ff100900 Add ZHA migration retry steps for unplugged adapters (#155537) 2025-11-04 20:37:52 +01:00
tronikos
1b62b2309f Remove Enmax Energy virtual integration (#155475) 2025-11-04 20:37:51 +01:00
puddly
9d57251aea Fix non-unique ZHA serial port paths and migrate USB integration to always list unique paths (#155019) 2025-11-04 20:37:50 +01:00
Bram Kragten
f877614e7f Bump version to 2025.11.0b3 2025-11-03 20:46:58 +01:00
Mike Degatano
170e1e87c7 Disable deprecated addon repair (#155739) 2025-11-03 20:46:03 +01:00
Michael Hansen
e1feba5c86 Use character code in language matching (voice) (#155738) 2025-11-03 20:46:02 +01:00
Bram Kragten
9bf52b7966 Update frontend to 20251103.0 (#155734) 2025-11-03 20:46:02 +01:00
Simone Chemelli
3bc61a3564 Bump aioamazondevices to 6.5.6 (#155723) 2025-11-03 20:46:01 +01:00
Bram Kragten
d2ba94e1bf Bump version to 2025.11.0b2 2025-11-03 08:04:32 +01:00
Joost Lekkerkerker
9a4ed82399 Bump python-open-router to 0.3.2 (#155700) 2025-11-03 08:04:12 +01:00
cdnninja
b5136d01aa fix vesync mist level value (#155697) 2025-11-03 08:04:11 +01:00
starkillerOG
d3e05090ea Bump reolink_aio to 0.16.3 (#155692) 2025-11-03 08:04:10 +01:00
Michael
7e75ca7af9 Revert "Remove neato integration (#154902)" (#155685) 2025-11-03 08:04:10 +01:00
Matthias Alphart
6616b5775f Fix KNX climate loading min/max temp from UI config (#155682) 2025-11-03 08:04:09 +01:00
Robert Resch
69b82d4c59 Bump deebot-client to 16.3.0 (#155681) 2025-11-03 08:04:08 +01:00
Bram Kragten
6b9709677a Fix device tracker name & icon for Volvo integration (#155667) 2025-11-03 08:03:00 +01:00
Robert Resch
a4e9c82c84 Bump deebot-client to 16.2.0 (#155642) 2025-11-03 07:57:45 +01:00
cdnninja
de86bedb80 vesync don't assume fan speed target (#155617) 2025-11-03 07:57:44 +01:00
Matthias Alphart
9111c6df90 Update knx-frontend to 2025.10.31.195356 (#155569) 2025-11-03 07:57:43 +01:00
Jordan Harvey
751f6bddb1 Update pynintendoparental to version 1.1.3 (#155568) 2025-11-03 07:57:42 +01:00
Josef Zweck
c9a61de0a1 Bump onedrive-personal-sdk to 0.0.15 (#155540) 2025-11-03 07:57:41 +01:00
Sid
01fb46d903 Bump eheimdigital to 1.4.0 (#155539) 2025-11-03 07:57:41 +01:00
cdnninja
d26f61c9fe Bump pyvesync to 3.1.4 (#155533) 2025-11-03 07:57:39 +01:00
Robert Resch
a47a144312 Bump uv to 0.9.6 (#155521) 2025-11-03 07:57:38 +01:00
Erwin Douna
69cf4f99d1 Portainer refactor CONF_VERIFY_SSL (#155520) 2025-11-03 07:57:37 +01:00
Shay Levy
e6c757c187 Fix Shelly irrigation zone ID retrieval with Sleepy devices (#155514) 2025-11-03 07:57:36 +01:00
hanwg
a36b0e2f3f Fix event entity state update for Telegram bot (#155510) 2025-11-03 07:57:35 +01:00
Jakob Schlyter
1a7c6cd96c Update regions and voices used by Amazon Polly (#155501) 2025-11-03 07:57:34 +01:00
tronikos
ba3e538402 Bump opower to 0.15.9 (#155473) 2025-11-03 07:57:33 +01:00
Mike Degatano
b2cd08aa65 Addon progress reporting follow-up from feedback (#155464) 2025-11-03 07:57:33 +01:00
karwosts
06dcd25a16 Hassfest check for invalid localization placeholders (#155216) 2025-11-03 07:57:32 +01:00
Bram Kragten
fd36782bae Bump version to 2025.11.0b1 2025-10-30 20:12:15 +01:00
Bram Kragten
ed4573db57 Update frontend to 20251029.1 (#155513) 2025-10-30 20:11:55 +01:00
Erwin Douna
78373a6483 Firefly fix config flow (#155503) 2025-10-30 20:11:54 +01:00
Sab44
8455c35bec Bump librehardwaremonitor-api to 1.5.0 (#155492) 2025-10-30 20:11:53 +01:00
Kinachi249
00887a2f3f Bump PyCync to 0.4.3 (#155477) 2025-10-30 20:11:52 +01:00
Erwin Douna
f1ca7543fa Bump pyportainer 1.0.12 (#155468) 2025-10-30 20:11:51 +01:00
Abílio Costa
bb72b24ba9 Mock async_setup_entry in BMW Connected Drive config flow test (#155446) 2025-10-30 20:11:50 +01:00
Andrea Turri
322a27d992 Miele RestoreSensor: restore native value rather than stringified state (#152750)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
2025-10-30 20:11:49 +01:00
hanwg
a3b516110b Deprecate legacy Telegram notify service (#150720)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: abmantis <amfcalt@gmail.com>
2025-10-30 20:11:48 +01:00
Bram Kragten
95ac5c0183 Bump version to 2025.11.0b0 2025-10-29 18:53:20 +01:00
Joakim Sørensen
40995b6d32 Bump hass-nabucasa from 1.4.0 to 1.5.1 (#155424)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-10-29 18:37:58 +01:00
Geoffrey
f64c029bd1 Update library version for VegeHub integration (#155360)
Co-authored-by: GhoweVege <85890024+GhoweVege@users.noreply.github.com>
2025-10-29 18:30:53 +01:00
torben-iometer
a050c0cd05 Add configuration_url to iometer (#155429)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-29 17:59:05 +01:00
Jordan Harvey
0c121468e0 Validate devices connected to Nintendo Parental Controls accounts (#154873)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-29 17:58:53 +01:00
Bram Kragten
af277651f8 Update frontend to 20251029.0 (#155432) 2025-10-29 17:42:14 +01:00
Michael Hansen
ca90826478 Use satellite area in fuzzy matcher (#155347)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-10-29 16:27:22 +01:00
Artur Pragacz
dd4789af4e Fix translation references to unverified translations (#155314)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-10-29 16:20:59 +01:00
puddly
ca49b6e7e2 Bump universal-silabs-flasher to 0.0.37 (#155421)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-10-29 16:09:05 +01:00
TheJulianJES
fdbe293483 Hide HA Connect Zigbee adapters in Z-Wave serial port selector (#154923)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-10-29 16:05:27 +01:00
Joakim Sørensen
aa67b46f6f Ensure api_server is defined in dev (#155422)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-10-29 16:02:05 +01:00
Shay Levy
9f21a97d39 Update IQS for Switcher integration to silver (#155425) 2025-10-29 14:42:24 +00:00
puddly
49158fad48 Fix ZBT and Yellow switch unit tests (#155426) 2025-10-29 15:33:20 +01:00
Jordan Harvey
dff8e5221b Use API token authentiation in traccar_server (#155297) 2025-10-29 15:10:47 +01:00
epenet
dbfa0aa22c Add missing data_description to sfr_box (#155420) 2025-10-29 16:05:51 +02:00
Shay Levy
ec4464d65f Mark Switcher removal instructions as done (#155414) 2025-10-29 15:04:55 +01:00
epenet
6c8dffd521 Add base entity to sfr_box (#155418) 2025-10-29 14:55:32 +01:00
Heindrich Paul
09763012fc Add reconfigure flow to nederlandse_spoorwegen (#155412)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-29 14:46:08 +01:00
puddly
aa5b970102 Beta firmware update switch for Connect integrations (#155370) 2025-10-29 14:45:56 +01:00
Robert Resch
80912045d7 Clear caplog in Ecovacs tests (#155404) 2025-10-29 14:33:43 +01:00
Jan-Philipp Benecke
dbda31f6d5 Attach Ping device tracker to Ping device (#155399) 2025-10-29 14:31:51 +01:00
Retha Runolfsson
24219dd8f7 Bump pySwitchbot to 0.72.1 (#155415) 2025-10-29 14:22:02 +01:00
adam-the-hero
77d0cf1573 Update Watergate dependency to 2025.1.0 (#155393)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-29 14:21:19 +01:00
Artur Pragacz
1ea534e400 Remove legacy platform support from translations (#155178) 2025-10-29 14:16:57 +01:00
epenet
a6ba8fa69c Add support for translation_placeholders in zha (#155254) 2025-10-29 14:08:04 +01:00
Michael
a6c1ce86d8 Remove neato integration (#154902) 2025-10-29 14:05:59 +01:00
puddly
fbb07e16cb Auto refresh hardware integration firmware update entities on setup (#154562) 2025-10-29 13:33:34 +01:00
Shay Levy
1387308f48 Update Switcher actions exceptions (#155296) 2025-10-29 14:32:10 +02:00
epenet
48d371eddb Use runtime_data in sfr_box (#155410) 2025-10-29 13:30:32 +01:00
Simone Chemelli
c45c11574c Add next alarm/reminder/timer sensors to Alexa Devices (#153576) 2025-10-29 13:11:25 +01:00
Ståle Storø Hauknes
14eb103338 Bump Airthings BLE to 1.2.0 (#155386) 2025-10-29 12:40:00 +01:00
Manu
2bdb258a39 Add image platform to Xbox integration (#155369)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-29 12:31:55 +01:00
Simone Chemelli
5b323526b6 Bump aioamazondevices to 6.5.5 (#155408) 2025-10-29 12:31:30 +01:00
TheJulianJES
9d434c9403 Bump ZHA to 0.0.75 (#155389) 2025-10-29 11:17:19 +00:00
epenet
e4103137ef Add sensors for Tuya cat toilet (#155245) 2025-10-29 11:50:38 +01:00
Jan-Philipp Benecke
d9d4cc9004 Use a config entry migration instead of migrating in async_setup in Ping (#155403)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-10-29 11:19:23 +01:00
246 changed files with 7305 additions and 1373 deletions

4
CODEOWNERS generated
View File

@@ -1543,8 +1543,8 @@ build.json @home-assistant/supervisor
/tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @home-assistant/core
/tests/components/sun/ @home-assistant/core
/homeassistant/components/sunricher_dali_center/ @niracler
/tests/components/sunricher_dali_center/ @niracler
/homeassistant/components/sunricher_dali/ @niracler
/tests/components/sunricher_dali/ @niracler
/homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen

2
Dockerfile generated
View File

@@ -31,7 +31,7 @@ RUN \
&& go2rtc --version
# Install uv
RUN pip3 install uv==0.9.5
RUN pip3 install uv==0.9.6
WORKDIR /usr/src

View File

@@ -28,5 +28,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling",
"requirements": ["airthings-ble==1.1.1"]
"requirements": ["airthings-ble==1.2.0"]
}

View File

@@ -26,3 +26,6 @@ COUNTRY_DOMAINS = {
"us": DEFAULT_DOMAIN,
"za": "co.za",
}
CATEGORY_SENSORS = "sensors"
CATEGORY_NOTIFICATIONS = "notifications"

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==6.4.6"]
"requirements": ["aioamazondevices==6.5.6"]
}

View File

@@ -4,9 +4,15 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import Final
from aioamazondevices.api import AmazonDevice
from aioamazondevices.const import (
NOTIFICATION_ALARM,
NOTIFICATION_REMINDER,
NOTIFICATION_TIMER,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -19,6 +25,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import CATEGORY_NOTIFICATIONS, CATEGORY_SENSORS
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
@@ -36,6 +43,20 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
)
category: str = CATEGORY_SENSORS
@dataclass(frozen=True, kw_only=True)
class AmazonNotificationEntityDescription(SensorEntityDescription):
"""Amazon Devices notification entity description."""
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
device.online
and (notification := device.notifications.get(key)) is not None
and notification.next_occurrence is not None
)
category: str = CATEGORY_NOTIFICATIONS
SENSORS: Final = (
@@ -56,6 +77,23 @@ SENSORS: Final = (
state_class=SensorStateClass.MEASUREMENT,
),
)
NOTIFICATIONS: Final = (
AmazonNotificationEntityDescription(
key=NOTIFICATION_ALARM,
translation_key="alarm",
device_class=SensorDeviceClass.TIMESTAMP,
),
AmazonNotificationEntityDescription(
key=NOTIFICATION_REMINDER,
translation_key="reminder",
device_class=SensorDeviceClass.TIMESTAMP,
),
AmazonNotificationEntityDescription(
key=NOTIFICATION_TIMER,
translation_key="timer",
device_class=SensorDeviceClass.TIMESTAMP,
),
)
async def async_setup_entry(
@@ -74,12 +112,18 @@ async def async_setup_entry(
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
sensors_list = [
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in SENSORS
for serial_num in new_devices
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
)
]
notifications_list = [
AmazonSensorEntity(coordinator, serial_num, notification_desc)
for notification_desc in NOTIFICATIONS
for serial_num in new_devices
]
async_add_entities(sensors_list + notifications_list)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
@@ -88,7 +132,9 @@ async def async_setup_entry(
class AmazonSensorEntity(AmazonEntity, SensorEntity):
"""Sensor device."""
entity_description: AmazonSensorEntityDescription
entity_description: (
AmazonSensorEntityDescription | AmazonNotificationEntityDescription
)
@property
def native_unit_of_measurement(self) -> str | None:
@@ -101,9 +147,13 @@ class AmazonSensorEntity(AmazonEntity, SensorEntity):
return super().native_unit_of_measurement
@property
def native_value(self) -> StateType:
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
return self.device.sensors[self.entity_description.key].value
# Sensors
if self.entity_description.category == CATEGORY_SENSORS:
return self.device.sensors[self.entity_description.key].value
# Notifications
return self.device.notifications[self.entity_description.key].next_occurrence
@property
def available(self) -> bool:

View File

@@ -66,6 +66,17 @@
"name": "Speak"
}
},
"sensor": {
"alarm": {
"name": "Next alarm"
},
"reminder": {
"name": "Next reminder"
},
"timer": {
"name": "Next timer"
}
},
"switch": {
"do_not_disturb": {
"name": "Do not disturb"

View File

@@ -106,7 +106,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="daily_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=2,
),
SensorEntityDescription(
@@ -150,7 +150,7 @@ SENSOR_DESCRIPTIONS = (
key=TYPE_LIGHTNING_PER_DAY,
translation_key="lightning_strikes_per_day",
native_unit_of_measurement="strikes",
state_class=SensorStateClass.TOTAL,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
@@ -182,7 +182,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="monthly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
@@ -229,7 +229,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="weekly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
@@ -262,7 +262,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="yearly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.3.0"]
"requirements": ["hassil==3.4.0"]
}

View File

@@ -189,7 +189,7 @@ class BryantEvolutionClimate(ClimateEntity):
return HVACAction.HEATING
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="failed_to_parse_hvac_mode",
translation_key="failed_to_parse_hvac_action",
translation_placeholders={
"mode_and_active": mode_and_active,
"current_temperature": str(self.current_temperature),

View File

@@ -24,7 +24,7 @@
},
"exceptions": {
"failed_to_parse_hvac_action": {
"message": "Could not determine HVAC action: {mode_and_active}, {self.current_temperature}, {self.target_temperature_low}"
"message": "Could not determine HVAC action: {mode_and_active}, {current_temperature}, {target_temperature_low}"
},
"failed_to_parse_hvac_mode": {
"message": "Cannot parse response to HVACMode: {mode}"

View File

@@ -115,26 +115,37 @@ GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend(
{vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA}}
)
_BASE_CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(CONF_COGNITO_CLIENT_ID): str,
vol.Optional(CONF_USER_POOL_ID): str,
vol.Optional(CONF_REGION): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
vol.Optional(CONF_ACCOUNT_LINK_SERVER): str,
vol.Optional(CONF_ACCOUNTS_SERVER): str,
vol.Optional(CONF_ACME_SERVER): str,
vol.Optional(CONF_API_SERVER): str,
vol.Optional(CONF_RELAYER_SERVER): str,
vol.Optional(CONF_REMOTESTATE_SERVER): str,
vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,
}
)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(
[MODE_DEV, MODE_PROD]
),
vol.Optional(CONF_COGNITO_CLIENT_ID): str,
vol.Optional(CONF_USER_POOL_ID): str,
vol.Optional(CONF_REGION): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
vol.Optional(CONF_ACCOUNT_LINK_SERVER): str,
vol.Optional(CONF_ACCOUNTS_SERVER): str,
vol.Optional(CONF_ACME_SERVER): str,
vol.Optional(CONF_API_SERVER): str,
vol.Optional(CONF_RELAYER_SERVER): str,
vol.Optional(CONF_REMOTESTATE_SERVER): str,
vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,
}
DOMAIN: vol.Any(
_BASE_CONFIG_SCHEMA.extend(
{
vol.Required(CONF_MODE): vol.In([MODE_DEV]),
vol.Required(CONF_API_SERVER): str,
}
),
_BASE_CONFIG_SCHEMA.extend(
{
vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In([MODE_PROD]),
}
),
)
},
extra=vol.ALLOW_EXTRA,

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.4.0"],
"requirements": ["hass-nabucasa==1.5.1"],
"single_config_entry": true
}

View File

@@ -768,7 +768,16 @@ class DefaultAgent(ConversationEntity):
if lang_intents.fuzzy_matcher is None:
return None
fuzzy_result = lang_intents.fuzzy_matcher.match(user_input.text)
context_area: str | None = None
satellite_area, _ = self._get_satellite_area_and_device(
user_input.satellite_id, user_input.device_id
)
if satellite_area:
context_area = satellite_area.name
fuzzy_result = lang_intents.fuzzy_matcher.match(
user_input.text, context_area=context_area
)
if fuzzy_result is None:
return None
@@ -1240,15 +1249,14 @@ class DefaultAgent(ConversationEntity):
intent_slot_list_names=self._fuzzy_config.slot_list_names,
slot_combinations={
intent_name: {
combo_key: [
SlotCombinationInfo(
name_domains=(
set(combo_info.name_domains)
if combo_info.name_domains
else None
)
)
]
combo_key: SlotCombinationInfo(
context_area=combo_info.context_area,
name_domains=(
set(combo_info.name_domains)
if combo_info.name_domains
else None
),
)
for combo_key, combo_info in intent_combos.items()
}
for intent_name, intent_combos in self._fuzzy_config.slot_combinations.items()

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.3.0", "home-assistant-intents==2025.10.28"]
"requirements": ["hassil==3.4.0", "home-assistant-intents==2025.10.28"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["pycync==0.4.2"]
"requirements": ["pycync==0.4.3"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==16.1.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==16.3.0"]
}

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["eheimdigital"],
"quality_scale": "platinum",
"requirements": ["eheimdigital==1.3.0"],
"requirements": ["eheimdigital==1.4.0"],
"zeroconf": [
{ "name": "eheimdigital._http._tcp.local.", "type": "_http._tcp.local." }
]

View File

@@ -1 +0,0 @@
"""Virtual integration: Enmax Energy."""

View File

@@ -1,6 +0,0 @@
{
"domain": "enmax",
"name": "Enmax Energy",
"integration_type": "virtual",
"supported_by": "opower"
}

View File

@@ -75,10 +75,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> b
async def async_unload_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> bool:
"""Unload an esphome config entry."""
entry_data = await cleanup_instance(entry)
return await hass.config_entries.async_unload_platforms(
entry, entry_data.loaded_platforms
unload_ok = await hass.config_entries.async_unload_platforms(
entry, entry.runtime_data.loaded_platforms
)
if unload_ok:
await cleanup_instance(entry)
return unload_ok
async def async_remove_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> None:

View File

@@ -40,7 +40,9 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
client = Firefly(
api_url=data[CONF_URL],
api_key=data[CONF_API_KEY],
session=async_get_clientsession(hass),
session=async_get_clientsession(
hass=hass, verify_ssl=data[CONF_VERIFY_SSL]
),
)
await client.get_about()
except FireflyAuthenticationError:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/foscam",
"iot_class": "local_polling",
"loggers": ["libpyfoscamcgi"],
"requirements": ["libpyfoscamcgi==0.0.8"]
"requirements": ["libpyfoscamcgi==0.0.9"]
}

View File

@@ -453,7 +453,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.http.app.router.register_resource(IndexView(repo_path, hass))
async_register_built_in_panel(hass, "light")
async_register_built_in_panel(hass, "safety")
async_register_built_in_panel(hass, "security")
async_register_built_in_panel(hass, "climate")
async_register_built_in_panel(hass, "profile")

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251001.4"]
"requirements": ["home-assistant-frontend==20251105.0"]
}

View File

@@ -620,7 +620,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
# Pop add-on data
# Unload coordinator
coordinator: HassioDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
coordinator.unload()
# Pop coordinator
hass.data.pop(ADDONS_COORDINATOR, None)
return unload_ok

View File

@@ -563,3 +563,8 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
self.async_set_updated_data(data)
except SupervisorError as err:
_LOGGER.warning("Could not refresh info for %s: %s", addon_slug, err)
@callback
def unload(self) -> None:
"""Clean up when config entry unloaded."""
self.jobs.unload()

View File

@@ -44,7 +44,6 @@ from .const import (
EVENT_SUPPORTED_CHANGED,
EXTRA_PLACEHOLDERS,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DEPRECATED,
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_PWNED,
@@ -87,7 +86,6 @@ ISSUE_KEYS_FOR_REPAIRS = {
"issue_system_disk_lifetime",
ISSUE_KEY_SYSTEM_FREE_SPACE,
ISSUE_KEY_ADDON_PWNED,
ISSUE_KEY_ADDON_DEPRECATED,
}
_LOGGER = logging.getLogger(__name__)

View File

@@ -3,6 +3,7 @@
from collections.abc import Callable
from dataclasses import dataclass, replace
from functools import partial
import logging
from typing import Any
from uuid import UUID
@@ -29,6 +30,8 @@ from .const import (
)
from .handler import get_supervisor_client
_LOGGER = logging.getLogger(__name__)
@dataclass(slots=True, frozen=True)
class JobSubscription:
@@ -45,7 +48,7 @@ class JobSubscription:
event_callback: Callable[[Job], Any]
uuid: str | None = None
name: str | None = None
reference: str | None | type[Any] = Any
reference: str | None = None
def __post_init__(self) -> None:
"""Validate at least one filter option is present."""
@@ -58,7 +61,7 @@ class JobSubscription:
"""Return true if job matches subscription filters."""
if self.uuid:
return job.uuid == self.uuid
return job.name == self.name and self.reference in (Any, job.reference)
return job.name == self.name and self.reference in (None, job.reference)
class SupervisorJobs:
@@ -70,6 +73,7 @@ class SupervisorJobs:
self._supervisor_client = get_supervisor_client(hass)
self._jobs: dict[UUID, Job] = {}
self._subscriptions: set[JobSubscription] = set()
self._dispatcher_disconnect: Callable[[], None] | None = None
@property
def current_jobs(self) -> list[Job]:
@@ -79,20 +83,24 @@ class SupervisorJobs:
def subscribe(self, subscription: JobSubscription) -> CALLBACK_TYPE:
"""Subscribe to updates for job. Return callback is used to unsubscribe.
If any jobs match the subscription at the time this is called, creates
tasks to run their callback on it.
If any jobs match the subscription at the time this is called, runs the
callback on them.
"""
self._subscriptions.add(subscription)
# As these are callbacks they are safe to run in the event loop
# We wrap these in an asyncio task so subscribing does not wait on the logic
if matches := [job for job in self._jobs.values() if subscription.matches(job)]:
async def event_callback_async(job: Job) -> Any:
return subscription.event_callback(job)
for match in matches:
self._hass.async_create_task(event_callback_async(match))
# Run the callback on each existing match
# We catch all errors to prevent an error in one from stopping the others
for match in [job for job in self._jobs.values() if subscription.matches(job)]:
try:
return subscription.event_callback(match)
except Exception as err: # noqa: BLE001
_LOGGER.error(
"Error encountered processing Supervisor Job (%s %s %s) - %s",
match.name,
match.reference,
match.uuid,
err,
)
return partial(self._subscriptions.discard, subscription)
@@ -131,7 +139,7 @@ class SupervisorJobs:
# If this is the first update register to receive Supervisor events
if first_update:
async_dispatcher_connect(
self._dispatcher_disconnect = async_dispatcher_connect(
self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_jobs
)
@@ -158,3 +166,14 @@ class SupervisorJobs:
for sub in self._subscriptions:
if sub.matches(job):
sub.event_callback(job)
# If the job is done, pop it from our cache if present after processing is done
if job.done and job.uuid in self._jobs:
del self._jobs[job.uuid]
@callback
def unload(self) -> None:
"""Unregister with dispatcher on config entry unload."""
if self._dispatcher_disconnect:
self._dispatcher_disconnect()
self._dispatcher_disconnect = None

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.83", "babel==2.15.0"]
"requirements": ["holidays==0.84", "babel==2.15.0"]
}

View File

@@ -2,20 +2,35 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
import os.path
from homeassistant.components.homeassistant_hardware.coordinator import (
FirmwareUpdateCoordinator,
)
from homeassistant.components.usb import USBDevice, async_register_port_event_callback
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import DEVICE, DOMAIN
from .const import DEVICE, DOMAIN, NABU_CASA_FIRMWARE_RELEASES_URL
_LOGGER = logging.getLogger(__name__)
type HomeAssistantConnectZBT2ConfigEntry = ConfigEntry[HomeAssistantConnectZBT2Data]
@dataclass
class HomeAssistantConnectZBT2Data:
"""Runtime data definition."""
coordinator: FirmwareUpdateCoordinator
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@@ -49,7 +64,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: HomeAssistantConnectZBT2ConfigEntry
) -> bool:
"""Set up a Home Assistant Connect ZBT-2 config entry."""
# Postpone loading the config entry if the device is missing
@@ -60,12 +77,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
translation_key="device_disconnected",
)
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
# Create and store the firmware update coordinator in runtime_data
session = async_get_clientsession(hass)
coordinator = FirmwareUpdateCoordinator(
hass,
entry,
session,
NABU_CASA_FIRMWARE_RELEASES_URL,
)
entry.runtime_data = HomeAssistantConnectZBT2Data(coordinator)
await hass.config_entries.async_forward_entry_setups(entry, ["switch", "update"])
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: HomeAssistantConnectZBT2ConfigEntry
) -> bool:
"""Unload a config entry."""
await hass.config_entries.async_unload_platforms(entry, ["update"])
return True
return await hass.config_entries.async_unload_platforms(entry, ["switch", "update"])

View File

@@ -39,6 +39,8 @@ from .const import (
NABU_CASA_FIRMWARE_RELEASES_URL,
PID,
PRODUCT,
RADIO_TX_POWER_DBM_BY_COUNTRY,
RADIO_TX_POWER_DBM_DEFAULT,
SERIAL_NUMBER,
VID,
)
@@ -75,6 +77,7 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
context: ConfigFlowContext
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR]
ZIGBEE_BAUDRATE = 460800
async def async_step_install_zigbee_firmware(
self, user_input: dict[str, Any] | None = None
@@ -102,6 +105,21 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
next_step_id="finish_thread_installation",
)
def _extra_zha_hardware_options(self) -> dict[str, Any]:
"""Return extra ZHA hardware options."""
country = self.hass.config.country
if country is None:
tx_power = RADIO_TX_POWER_DBM_DEFAULT
else:
tx_power = RADIO_TX_POWER_DBM_BY_COUNTRY.get(
country, RADIO_TX_POWER_DBM_DEFAULT
)
return {
"tx_power": tx_power,
}
class HomeAssistantConnectZBT2ConfigFlow(
ZBT2FirmwareMixin,
@@ -112,7 +130,6 @@ class HomeAssistantConnectZBT2ConfigFlow(
VERSION = 1
MINOR_VERSION = 1
ZIGBEE_BAUDRATE = 460800
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the config flow."""

View File

@@ -1,9 +1,11 @@
"""Constants for the Home Assistant Connect ZBT-2 integration."""
from homeassistant.generated.countries import COUNTRIES
DOMAIN = "homeassistant_connect_zbt2"
NABU_CASA_FIRMWARE_RELEASES_URL = (
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases/latest"
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases"
)
FIRMWARE = "firmware"
@@ -17,3 +19,59 @@ VID = "vid"
DEVICE = "device"
HARDWARE_NAME = "Home Assistant Connect ZBT-2"
RADIO_TX_POWER_DBM_DEFAULT = 8
RADIO_TX_POWER_DBM_BY_COUNTRY = {
# EU Member States
"AT": 10,
"BE": 10,
"BG": 10,
"HR": 10,
"CY": 10,
"CZ": 10,
"DK": 10,
"EE": 10,
"FI": 10,
"FR": 10,
"DE": 10,
"GR": 10,
"HU": 10,
"IE": 10,
"IT": 10,
"LV": 10,
"LT": 10,
"LU": 10,
"MT": 10,
"NL": 10,
"PL": 10,
"PT": 10,
"RO": 10,
"SK": 10,
"SI": 10,
"ES": 10,
"SE": 10,
# EEA Members
"IS": 10,
"LI": 10,
"NO": 10,
# Standards harmonized with RED or ETSI
"CH": 10,
"GB": 10,
"TR": 10,
"AL": 10,
"BA": 10,
"GE": 10,
"MD": 10,
"ME": 10,
"MK": 10,
"RS": 10,
"UA": 10,
# Other CEPT nations
"AD": 10,
"AZ": 10,
"MC": 10,
"SM": 10,
"VA": 10,
}
assert set(RADIO_TX_POWER_DBM_BY_COUNTRY) <= COUNTRIES

View File

@@ -0,0 +1,9 @@
{
"entity": {
"switch": {
"beta_firmware": {
"default": "mdi:test-tube"
}
}
}
}

View File

@@ -90,6 +90,13 @@
}
}
},
"entity": {
"switch": {
"beta_firmware": {
"name": "Beta firmware updates"
}
}
},
"exceptions": {
"device_disconnected": {
"message": "The device is not plugged in"

View File

@@ -0,0 +1,54 @@
"""Home Assistant Connect ZBT-2 switch entities."""
from __future__ import annotations
import logging
from homeassistant.components.homeassistant_hardware.coordinator import (
FirmwareUpdateCoordinator,
)
from homeassistant.components.homeassistant_hardware.switch import (
BaseBetaFirmwareSwitch,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeAssistantConnectZBT2ConfigEntry
from .const import DOMAIN, HARDWARE_NAME, SERIAL_NUMBER
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeAssistantConnectZBT2ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the switch platform for Home Assistant Connect ZBT-2."""
async_add_entities(
[BetaFirmwareSwitch(config_entry.runtime_data.coordinator, config_entry)]
)
class BetaFirmwareSwitch(BaseBetaFirmwareSwitch):
"""Home Assistant Connect ZBT-2 beta firmware switch."""
def __init__(
self,
coordinator: FirmwareUpdateCoordinator,
config_entry: HomeAssistantConnectZBT2ConfigEntry,
) -> None:
"""Initialize the beta firmware switch."""
super().__init__(coordinator, config_entry)
serial_number = self._config_entry.data[SERIAL_NUMBER]
self._attr_unique_id = f"{serial_number}_beta_firmware"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_number)},
name=f"{HARDWARE_NAME} ({serial_number})",
model=HARDWARE_NAME,
manufacturer="Nabu Casa",
serial_number=serial_number,
)

View File

@@ -4,8 +4,6 @@ from __future__ import annotations
import logging
import aiohttp
from homeassistant.components.homeassistant_hardware.coordinator import (
FirmwareUpdateCoordinator,
)
@@ -19,22 +17,14 @@ from homeassistant.components.homeassistant_hardware.util import (
ResetTarget,
)
from homeassistant.components.update import UpdateDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
DOMAIN,
FIRMWARE,
FIRMWARE_VERSION,
HARDWARE_NAME,
NABU_CASA_FIRMWARE_RELEASES_URL,
SERIAL_NUMBER,
)
from . import HomeAssistantConnectZBT2ConfigEntry
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, HARDWARE_NAME, SERIAL_NUMBER
_LOGGER = logging.getLogger(__name__)
@@ -91,8 +81,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
def _async_create_update_entity(
hass: HomeAssistant,
config_entry: ConfigEntry,
session: aiohttp.ClientSession,
config_entry: HomeAssistantConnectZBT2ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> FirmwareUpdateEntity:
"""Create an update entity that handles firmware type changes."""
@@ -111,12 +100,7 @@ def _async_create_update_entity(
entity = FirmwareUpdateEntity(
device=config_entry.data["device"],
config_entry=config_entry,
update_coordinator=FirmwareUpdateCoordinator(
hass,
config_entry,
session,
NABU_CASA_FIRMWARE_RELEASES_URL,
),
update_coordinator=config_entry.runtime_data.coordinator,
entity_description=entity_description,
)
@@ -126,11 +110,7 @@ def _async_create_update_entity(
"""Replace the current entity when the firmware type changes."""
er.async_get(hass).async_remove(entity.entity_id)
async_add_entities(
[
_async_create_update_entity(
hass, config_entry, session, async_add_entities
)
]
[_async_create_update_entity(hass, config_entry, async_add_entities)]
)
entity.async_on_remove(
@@ -142,14 +122,11 @@ def _async_create_update_entity(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: HomeAssistantConnectZBT2ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the firmware update config entry."""
session = async_get_clientsession(hass)
entity = _async_create_update_entity(
hass, config_entry, session, async_add_entities
)
entity = _async_create_update_entity(hass, config_entry, async_add_entities)
async_add_entities([entity])
@@ -162,7 +139,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
def __init__(
self,
device: str,
config_entry: ConfigEntry,
config_entry: HomeAssistantConnectZBT2ConfigEntry,
update_coordinator: FirmwareUpdateCoordinator,
entity_description: FirmwareUpdateEntityDescription,
) -> None:

View File

@@ -456,6 +456,10 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
# This step is necessary to prevent `user_input` from being passed through
return await self.async_step_continue_zigbee()
def _extra_zha_hardware_options(self) -> dict[str, Any]:
"""Return extra ZHA hardware options."""
return {}
async def async_step_continue_zigbee(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -478,6 +482,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
},
"radio_type": "ezsp",
"flow_strategy": self._zigbee_flow_strategy,
**self._extra_zha_hardware_options(),
},
)
return self._continue_zha_flow(result)

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
"integration_type": "system",
"requirements": [
"universal-silabs-flasher==0.0.35",
"ha-silabs-firmware-client==0.2.0"
"universal-silabs-flasher==0.0.37",
"ha-silabs-firmware-client==0.3.0"
]
}

View File

@@ -0,0 +1,64 @@
"""Home Assistant Hardware base beta firmware switch entity."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.helpers.restore_state import RestoreEntity
from .coordinator import FirmwareUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
class BaseBetaFirmwareSwitch(SwitchEntity, RestoreEntity):
"""Base switch to enable beta firmware updates."""
_attr_has_entity_name = True
_attr_entity_category = EntityCategory.CONFIG
_attr_entity_registry_enabled_default = False
_attr_translation_key = "beta_firmware"
def __init__(
self,
coordinator: FirmwareUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the beta firmware switch."""
self._coordinator = coordinator
self._config_entry = config_entry
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added to hass."""
await super().async_added_to_hass()
# Restore the last state
last_state = await self.async_get_last_state()
if last_state is not None:
self._attr_is_on = last_state.state == "on"
else:
self._attr_is_on = False
# Apply the restored state to the coordinator
await self._update_coordinator_prerelease()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on beta firmware updates."""
self._attr_is_on = True
self.async_write_ha_state()
await self._update_coordinator_prerelease()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off beta firmware updates."""
self._attr_is_on = False
self.async_write_ha_state()
await self._update_coordinator_prerelease()
async def _update_coordinator_prerelease(self) -> None:
"""Update the coordinator with the current prerelease setting."""
self._coordinator.client.update_prerelease(bool(self._attr_is_on))
await self._coordinator.async_refresh()

View File

@@ -150,6 +150,11 @@ class BaseFirmwareUpdateEntity(
self._update_attributes()
# Fetch firmware info early to avoid prolonged "unknown" state when the device
# is initially set up
if self._latest_manifest is None:
await self.coordinator.async_request_refresh()
@property
def extra_restore_state_data(self) -> FirmwareUpdateExtraStoredData:
"""Return state data to be restored."""

View File

@@ -2,9 +2,13 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
import os.path
from homeassistant.components.homeassistant_hardware.coordinator import (
FirmwareUpdateCoordinator,
)
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
from homeassistant.components.usb import (
USBDevice,
@@ -15,6 +19,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import (
@@ -24,6 +29,7 @@ from .const import (
FIRMWARE,
FIRMWARE_VERSION,
MANUFACTURER,
NABU_CASA_FIRMWARE_RELEASES_URL,
PID,
PRODUCT,
SERIAL_NUMBER,
@@ -32,6 +38,16 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
type HomeAssistantSkyConnectConfigEntry = ConfigEntry[HomeAssistantSkyConnectData]
@dataclass
class HomeAssistantSkyConnectData:
"""Runtime data definition."""
coordinator: FirmwareUpdateCoordinator
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@@ -65,7 +81,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: HomeAssistantSkyConnectConfigEntry
) -> bool:
"""Set up a Home Assistant SkyConnect config entry."""
# Postpone loading the config entry if the device is missing
@@ -76,18 +94,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
translation_key="device_disconnected",
)
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
# Create and store the firmware update coordinator in runtime_data
session = async_get_clientsession(hass)
coordinator = FirmwareUpdateCoordinator(
hass,
entry,
session,
NABU_CASA_FIRMWARE_RELEASES_URL,
)
entry.runtime_data = HomeAssistantSkyConnectData(coordinator)
await hass.config_entries.async_forward_entry_setups(entry, ["switch", "update"])
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: HomeAssistantSkyConnectConfigEntry
) -> bool:
"""Unload a config entry."""
await hass.config_entries.async_unload_platforms(entry, ["update"])
return True
return await hass.config_entries.async_unload_platforms(entry, ["switch", "update"])
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_migrate_entry(
hass: HomeAssistant, config_entry: HomeAssistantSkyConnectConfigEntry
) -> bool:
"""Migrate old entry."""
_LOGGER.debug(

View File

@@ -8,7 +8,7 @@ DOMAIN = "homeassistant_sky_connect"
DOCS_WEB_FLASHER_URL = "https://skyconnect.home-assistant.io/firmware-update/"
NABU_CASA_FIRMWARE_RELEASES_URL = (
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases/latest"
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases"
)
FIRMWARE = "firmware"

View File

@@ -0,0 +1,9 @@
{
"entity": {
"switch": {
"beta_firmware": {
"default": "mdi:test-tube"
}
}
}
}

View File

@@ -90,6 +90,13 @@
}
}
},
"entity": {
"switch": {
"beta_firmware": {
"name": "Beta firmware updates"
}
}
},
"exceptions": {
"device_disconnected": {
"message": "The device is not plugged in"

View File

@@ -0,0 +1,57 @@
"""Home Assistant SkyConnect switch entities."""
from __future__ import annotations
import logging
from homeassistant.components.homeassistant_hardware.coordinator import (
FirmwareUpdateCoordinator,
)
from homeassistant.components.homeassistant_hardware.switch import (
BaseBetaFirmwareSwitch,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeAssistantSkyConnectConfigEntry
from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, HardwareVariant
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeAssistantSkyConnectConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the switch platform for Home Assistant SkyConnect."""
async_add_entities(
[BetaFirmwareSwitch(config_entry.runtime_data.coordinator, config_entry)]
)
class BetaFirmwareSwitch(BaseBetaFirmwareSwitch):
"""Home Assistant SkyConnect beta firmware switch."""
def __init__(
self,
coordinator: FirmwareUpdateCoordinator,
config_entry: HomeAssistantSkyConnectConfigEntry,
) -> None:
"""Initialize the beta firmware switch."""
super().__init__(coordinator, config_entry)
variant = HardwareVariant.from_usb_product_name(
self._config_entry.data[PRODUCT]
)
serial_number = self._config_entry.data[SERIAL_NUMBER]
self._attr_unique_id = f"{serial_number}_beta_firmware"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_number)},
name=f"{variant.full_name} ({serial_number[:8]})",
model=variant.full_name,
manufacturer="Nabu Casa",
serial_number=serial_number,
)

View File

@@ -4,8 +4,6 @@ from __future__ import annotations
import logging
import aiohttp
from homeassistant.components.homeassistant_hardware.coordinator import (
FirmwareUpdateCoordinator,
)
@@ -18,19 +16,17 @@ from homeassistant.components.homeassistant_hardware.util import (
FirmwareInfo,
)
from homeassistant.components.update import UpdateDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeAssistantSkyConnectConfigEntry
from .const import (
DOMAIN,
FIRMWARE,
FIRMWARE_VERSION,
NABU_CASA_FIRMWARE_RELEASES_URL,
PRODUCT,
SERIAL_NUMBER,
HardwareVariant,
@@ -102,8 +98,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
def _async_create_update_entity(
hass: HomeAssistant,
config_entry: ConfigEntry,
session: aiohttp.ClientSession,
config_entry: HomeAssistantSkyConnectConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> FirmwareUpdateEntity:
"""Create an update entity that handles firmware type changes."""
@@ -122,12 +117,7 @@ def _async_create_update_entity(
entity = FirmwareUpdateEntity(
device=config_entry.data["device"],
config_entry=config_entry,
update_coordinator=FirmwareUpdateCoordinator(
hass,
config_entry,
session,
NABU_CASA_FIRMWARE_RELEASES_URL,
),
update_coordinator=config_entry.runtime_data.coordinator,
entity_description=entity_description,
)
@@ -137,11 +127,7 @@ def _async_create_update_entity(
"""Replace the current entity when the firmware type changes."""
er.async_get(hass).async_remove(entity.entity_id)
async_add_entities(
[
_async_create_update_entity(
hass, config_entry, session, async_add_entities
)
]
[_async_create_update_entity(hass, config_entry, async_add_entities)]
)
entity.async_on_remove(
@@ -153,14 +139,11 @@ def _async_create_update_entity(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: HomeAssistantSkyConnectConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the firmware update config entry."""
session = async_get_clientsession(hass)
entity = _async_create_update_entity(
hass, config_entry, session, async_add_entities
)
entity = _async_create_update_entity(hass, config_entry, async_add_entities)
async_add_entities([entity])
@@ -174,7 +157,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
def __init__(
self,
device: str,
config_entry: ConfigEntry,
config_entry: HomeAssistantSkyConnectConfigEntry,
update_coordinator: FirmwareUpdateCoordinator,
entity_description: FirmwareUpdateEntityDescription,
) -> None:

View File

@@ -2,9 +2,13 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
from homeassistant.components.hassio import get_os_info
from homeassistant.components.homeassistant_hardware.coordinator import (
FirmwareUpdateCoordinator,
)
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
check_multi_pan_addon,
)
@@ -16,14 +20,34 @@ from homeassistant.config_entries import SOURCE_HARDWARE, ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import discovery_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.hassio import is_hassio
from .const import FIRMWARE, FIRMWARE_VERSION, RADIO_DEVICE, ZHA_HW_DISCOVERY_DATA
from .const import (
FIRMWARE,
FIRMWARE_VERSION,
NABU_CASA_FIRMWARE_RELEASES_URL,
RADIO_DEVICE,
ZHA_HW_DISCOVERY_DATA,
)
_LOGGER = logging.getLogger(__name__)
type HomeAssistantYellowConfigEntry = ConfigEntry[HomeAssistantYellowData]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@dataclass
class HomeAssistantYellowData:
"""Runtime data definition."""
coordinator: (
FirmwareUpdateCoordinator # Type from homeassistant_hardware.coordinator
)
async def async_setup_entry(
hass: HomeAssistant, entry: HomeAssistantYellowConfigEntry
) -> bool:
"""Set up a Home Assistant Yellow config entry."""
if not is_hassio(hass):
# Not running under supervisor, Home Assistant may have been migrated
@@ -56,18 +80,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
data=ZHA_HW_DISCOVERY_DATA,
)
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
# Create and store the firmware update coordinator in runtime_data
session = async_get_clientsession(hass)
coordinator = FirmwareUpdateCoordinator(
hass,
entry,
session,
NABU_CASA_FIRMWARE_RELEASES_URL,
)
entry.runtime_data = HomeAssistantYellowData(coordinator)
await hass.config_entries.async_forward_entry_setups(entry, ["switch", "update"])
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: HomeAssistantYellowConfigEntry
) -> bool:
"""Unload a config entry."""
await hass.config_entries.async_unload_platforms(entry, ["update"])
return True
return await hass.config_entries.async_unload_platforms(entry, ["switch", "update"])
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_migrate_entry(
hass: HomeAssistant, config_entry: HomeAssistantYellowConfigEntry
) -> bool:
"""Migrate old entry."""
_LOGGER.debug(

View File

@@ -22,5 +22,5 @@ FIRMWARE_VERSION = "firmware_version"
ZHA_DOMAIN = "zha"
NABU_CASA_FIRMWARE_RELEASES_URL = (
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases/latest"
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases"
)

View File

@@ -0,0 +1,9 @@
{
"entity": {
"switch": {
"beta_firmware": {
"default": "mdi:test-tube"
}
}
}
}

View File

@@ -1,5 +1,10 @@
{
"entity": {
"switch": {
"beta_firmware": {
"name": "Radio beta firmware updates"
}
},
"update": {
"radio_firmware": {
"name": "Radio firmware"

View File

@@ -0,0 +1,50 @@
"""Home Assistant Yellow switch entities."""
from __future__ import annotations
import logging
from homeassistant.components.homeassistant_hardware.coordinator import (
FirmwareUpdateCoordinator,
)
from homeassistant.components.homeassistant_hardware.switch import (
BaseBetaFirmwareSwitch,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeAssistantYellowConfigEntry
from .const import DOMAIN, MANUFACTURER, MODEL
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeAssistantYellowConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the switch platform for Home Assistant Yellow."""
async_add_entities(
[BetaFirmwareSwitch(config_entry.runtime_data.coordinator, config_entry)]
)
class BetaFirmwareSwitch(BaseBetaFirmwareSwitch):
"""Home Assistant Yellow beta firmware switch."""
def __init__(
self,
coordinator: FirmwareUpdateCoordinator,
config_entry: HomeAssistantYellowConfigEntry,
) -> None:
"""Initialize the beta firmware switch."""
super().__init__(coordinator, config_entry)
self._attr_unique_id = "beta_firmware"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, "yellow")},
name=MODEL,
model=MODEL,
manufacturer=MANUFACTURER,
)

View File

@@ -4,8 +4,6 @@ from __future__ import annotations
import logging
import aiohttp
from homeassistant.components.homeassistant_hardware.coordinator import (
FirmwareUpdateCoordinator,
)
@@ -19,23 +17,14 @@ from homeassistant.components.homeassistant_hardware.util import (
ResetTarget,
)
from homeassistant.components.update import UpdateDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
DOMAIN,
FIRMWARE,
FIRMWARE_VERSION,
MANUFACTURER,
MODEL,
NABU_CASA_FIRMWARE_RELEASES_URL,
RADIO_DEVICE,
)
from . import HomeAssistantYellowConfigEntry
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, MANUFACTURER, MODEL, RADIO_DEVICE
_LOGGER = logging.getLogger(__name__)
@@ -108,8 +97,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
def _async_create_update_entity(
hass: HomeAssistant,
config_entry: ConfigEntry,
session: aiohttp.ClientSession,
config_entry: HomeAssistantYellowConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> FirmwareUpdateEntity:
"""Create an update entity that handles firmware type changes."""
@@ -128,12 +116,7 @@ def _async_create_update_entity(
entity = FirmwareUpdateEntity(
device=RADIO_DEVICE,
config_entry=config_entry,
update_coordinator=FirmwareUpdateCoordinator(
hass,
config_entry,
session,
NABU_CASA_FIRMWARE_RELEASES_URL,
),
update_coordinator=config_entry.runtime_data.coordinator,
entity_description=entity_description,
)
@@ -143,11 +126,7 @@ def _async_create_update_entity(
"""Replace the current entity when the firmware type changes."""
er.async_get(hass).async_remove(entity.entity_id)
async_add_entities(
[
_async_create_update_entity(
hass, config_entry, session, async_add_entities
)
]
[_async_create_update_entity(hass, config_entry, async_add_entities)]
)
entity.async_on_remove(
@@ -159,14 +138,11 @@ def _async_create_update_entity(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: HomeAssistantYellowConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the firmware update config entry."""
session = async_get_clientsession(hass)
entity = _async_create_update_entity(
hass, config_entry, session, async_add_entities
)
entity = _async_create_update_entity(hass, config_entry, async_add_entities)
async_add_entities([entity])
@@ -179,7 +155,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
def __init__(
self,
device: str,
config_entry: ConfigEntry,
config_entry: HomeAssistantYellowConfigEntry,
update_coordinator: FirmwareUpdateCoordinator,
entity_description: FirmwareUpdateEntityDescription,
) -> None:

View File

@@ -38,6 +38,7 @@ from homeassistant.const import (
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, issue_registry as ir, storage
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.http import (
KEY_ALLOW_CONFIGURED_CORS,
KEY_AUTHENTICATED, # noqa: F401
@@ -109,7 +110,7 @@ HTTP_SCHEMA: Final = vol.All(
cv.deprecated(CONF_BASE_URL),
vol.Schema(
{
vol.Optional(CONF_SERVER_HOST, default=_DEFAULT_BIND): vol.All(
vol.Optional(CONF_SERVER_HOST): vol.All(
cv.ensure_list, vol.Length(min=1), [cv.string]
),
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port,
@@ -207,7 +208,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if conf is None:
conf = cast(ConfData, HTTP_SCHEMA({}))
server_host = conf[CONF_SERVER_HOST]
if CONF_SERVER_HOST in conf and is_hassio(hass):
ir.async_create_issue(
hass,
DOMAIN,
"server_host_may_break_hassio",
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="server_host_may_break_hassio",
)
server_host = conf.get(CONF_SERVER_HOST, _DEFAULT_BIND)
server_port = conf[CONF_SERVER_PORT]
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE)

View File

@@ -1,5 +1,9 @@
{
"issues": {
"server_host_may_break_hassio": {
"description": "The `server_host` configuration option in the HTTP integration is prone to break the communication between Home Assistant Core and Supervisor, and will be removed in a future release.\n\nIf you are using this option to bind Home Assistant to specific network interfaces, please remove it from your configuration. Home Assistant will automatically bind to all available interfaces by default.\n\nIf you have specific networking requirements, consider using firewall rules or other network configuration to control access to Home Assistant.",
"title": "The `server_host` HTTP configuration may break Home Assistant Core - Supervisor communication"
},
"ssl_configured_without_configured_urls": {
"description": "Home Assistant detected that SSL has been set up on your instance, however, no custom external internet URL has been set.\n\nThis may result in unexpected behavior. Text-to-speech may fail, and integrations may not be able to connect back to your instance correctly.\n\nTo address this issue, go to Settings > System > Network; under the \"Home Assistant URL\" section, configure your new \"Internet\" and \"Local network\" addresses that match your new SSL configuration.",
"title": "SSL is configured without an external URL or internal URL"

View File

@@ -1,5 +1,6 @@
"""Base class for IOmeter entities."""
from homeassistant.const import CONF_HOST
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -21,4 +22,5 @@ class IOmeterEntity(CoordinatorEntity[IOMeterCoordinator]):
manufacturer="IOmeter GmbH",
model="IOmeter",
sw_version=coordinator.current_fw_version,
configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}/",
)

View File

@@ -299,8 +299,8 @@ def _create_climate_ui(xknx: XKNX, conf: ConfigExtractor, name: str) -> XknxClim
group_address_active_state=conf.get_state_and_passive(CONF_GA_ACTIVE),
group_address_command_value_state=conf.get_state_and_passive(CONF_GA_VALVE),
sync_state=sync_state,
min_temp=conf.get(ClimateConf.MIN_TEMP),
max_temp=conf.get(ClimateConf.MAX_TEMP),
min_temp=conf.get(CONF_TARGET_TEMPERATURE, ClimateConf.MIN_TEMP),
max_temp=conf.get(CONF_TARGET_TEMPERATURE, ClimateConf.MAX_TEMP),
mode=climate_mode,
group_address_fan_speed=conf.get_write(CONF_GA_FAN_SPEED),
group_address_fan_speed_state=conf.get_state_and_passive(CONF_GA_FAN_SPEED),
@@ -486,7 +486,7 @@ class _KnxClimate(ClimateEntity, _KnxEntityBase):
ha_controller_modes.append(self._last_hvac_mode)
ha_controller_modes.append(HVACMode.OFF)
hvac_modes = list(set(filter(None, ha_controller_modes)))
hvac_modes = sorted(set(filter(None, ha_controller_modes)))
return (
hvac_modes
if hvac_modes

View File

@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.10.0",
"xknxproject==3.8.2",
"knx-frontend==2025.10.26.81530"
"knx-frontend==2025.10.31.195356"
],
"single_config_entry": true
}

View File

@@ -622,6 +622,7 @@ ENERGY_USAGE_SENSORS: tuple[ThinQEnergySensorEntityDescription, ...] = (
usage_period=USAGE_MONTHLY,
start_date_fn=lambda today: today,
end_date_fn=lambda today: today,
state_class=SensorStateClass.TOTAL_INCREASING,
),
ThinQEnergySensorEntityDescription(
key="last_month",

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/libre_hardware_monitor",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["librehardwaremonitor-api==1.4.0"]
"requirements": ["librehardwaremonitor-api==1.5.0"]
}

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from typing import Any
from librehardwaremonitor_api.model import LibreHardwareMonitorSensorData
from homeassistant.components.sensor import SensorEntity, SensorStateClass
@@ -51,10 +53,10 @@ class LibreHardwareMonitorSensor(
super().__init__(coordinator)
self._attr_name: str = sensor_data.name
self.value: str | None = sensor_data.value
self._attr_extra_state_attributes: dict[str, str] = {
STATE_MIN_VALUE: self._format_number_value(sensor_data.min),
STATE_MAX_VALUE: self._format_number_value(sensor_data.max),
self._attr_native_value: str | None = sensor_data.value
self._attr_extra_state_attributes: dict[str, Any] = {
STATE_MIN_VALUE: sensor_data.min,
STATE_MAX_VALUE: sensor_data.max,
}
self._attr_native_unit_of_measurement = sensor_data.unit
self._attr_unique_id: str = f"{entry_id}_{sensor_data.sensor_id}"
@@ -72,23 +74,12 @@ class LibreHardwareMonitorSensor(
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if sensor_data := self.coordinator.data.sensor_data.get(self._sensor_id):
self.value = sensor_data.value
self._attr_native_value = sensor_data.value
self._attr_extra_state_attributes = {
STATE_MIN_VALUE: self._format_number_value(sensor_data.min),
STATE_MAX_VALUE: self._format_number_value(sensor_data.max),
STATE_MIN_VALUE: sensor_data.min,
STATE_MAX_VALUE: sensor_data.max,
}
else:
self.value = None
self._attr_native_value = None
super()._handle_coordinator_update()
@property
def native_value(self) -> str | None:
"""Return the formatted sensor value or None if no value is available."""
if self.value is not None and self.value != "-":
return self._format_number_value(self.value)
return None
@staticmethod
def _format_number_value(number_str: str) -> str:
return number_str.replace(",", ".")

View File

@@ -13,5 +13,5 @@
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"quality_scale": "bronze",
"requirements": ["pylitterbot==2024.2.7"]
"requirements": ["pylitterbot==2025.0.0"]
}

View File

@@ -9,7 +9,6 @@ from typing import Any
from chip.clusters import Objects as clusters
from chip.clusters.Objects import ClusterCommand, NullValue
from matter_server.client.models import device_types
from matter_server.common.errors import MatterError
from homeassistant.components.switch import (
SwitchDeviceClass,
@@ -19,7 +18,6 @@ from homeassistant.components.switch import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import MatterEntity, MatterEntityDescription
@@ -56,21 +54,15 @@ class MatterSwitch(MatterEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn switch on."""
try:
await self.send_device_command(
clusters.OnOff.Commands.On(),
)
except MatterError as err:
raise HomeAssistantError(f"Failed to set value: {err}") from err
await self.send_device_command(
clusters.OnOff.Commands.On(),
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn switch off."""
try:
await self.send_device_command(
clusters.OnOff.Commands.Off(),
)
except MatterError as err:
raise HomeAssistantError(f"Failed to set value: {err}") from err
await self.send_device_command(
clusters.OnOff.Commands.Off(),
)
@callback
def _update_from_device(self) -> None:
@@ -91,24 +83,18 @@ class MatterGenericCommandSwitch(MatterSwitch):
"""Turn switch on."""
if self.entity_description.on_command:
# custom command defined to set the new value
try:
await self.send_device_command(
self.entity_description.on_command(),
self.entity_description.command_timeout,
)
except MatterError as err:
raise HomeAssistantError(f"Failed to set value: {err}") from err
await self.send_device_command(
self.entity_description.on_command(),
self.entity_description.command_timeout,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn switch off."""
if self.entity_description.off_command:
try:
await self.send_device_command(
self.entity_description.off_command(),
self.entity_description.command_timeout,
)
except MatterError as err:
raise HomeAssistantError(f"Failed to set value: {err}") from err
await self.send_device_command(
self.entity_description.off_command(),
self.entity_description.command_timeout,
)
@callback
def _update_from_device(self) -> None:
@@ -125,16 +111,13 @@ class MatterGenericCommandSwitch(MatterSwitch):
**kwargs: Any,
) -> None:
"""Send device command with timeout."""
try:
await self.matter_client.send_device_command(
node_id=self._endpoint.node.node_id,
endpoint_id=self._endpoint.endpoint_id,
command=command,
timed_request_timeout_ms=command_timeout,
**kwargs,
)
except MatterError as err:
raise HomeAssistantError(f"Failed to set value: {err}") from err
await self.matter_client.send_device_command(
node_id=self._endpoint.node.node_id,
endpoint_id=self._endpoint.endpoint_id,
command=command,
timed_request_timeout_ms=command_timeout,
**kwargs,
)
@dataclass(frozen=True, kw_only=True)

View File

@@ -19,7 +19,6 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
STATE_UNKNOWN,
EntityCategory,
UnitOfEnergy,
UnitOfTemperature,
@@ -762,40 +761,35 @@ class MieleSensor(MieleEntity, SensorEntity):
class MieleRestorableSensor(MieleSensor, RestoreSensor):
"""Representation of a Sensor whose internal state can be restored."""
_last_value: StateType
def __init__(
self,
coordinator: MieleDataUpdateCoordinator,
device_id: str,
description: MieleSensorDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, device_id, description)
self._last_value = None
_attr_native_value: StateType
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
# recover last value from cache when adding entity
last_value = await self.async_get_last_state()
if last_value and last_value.state != STATE_UNKNOWN:
self._last_value = last_value.state
last_data = await self.async_get_last_sensor_data()
if last_data:
self._attr_native_value = last_data.native_value # type: ignore[assignment]
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self._last_value
"""Return the state of the sensor.
def _update_last_value(self) -> None:
"""Update the last value of the sensor."""
self._last_value = self.entity_description.value_fn(self.device)
It is necessary to override `native_value` to fall back to the default
attribute-based implementation, instead of the function-based
implementation in `MieleSensor`.
"""
return self._attr_native_value
def _update_native_value(self) -> None:
"""Update the native value attribute of the sensor."""
self._attr_native_value = self.entity_description.value_fn(self.device)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_last_value()
self._update_native_value()
super()._handle_coordinator_update()
@@ -912,7 +906,7 @@ class MieleProgramIdSensor(MieleSensor):
class MieleTimeSensor(MieleRestorableSensor):
"""Representation of time sensors keeping state from cache."""
def _update_last_value(self) -> None:
def _update_native_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
@@ -923,7 +917,9 @@ class MieleTimeSensor(MieleRestorableSensor):
current_status == StateStatus.PROGRAM_ENDED
and self.entity_description.end_value_fn is not None
):
self._last_value = self.entity_description.end_value_fn(self._last_value)
self._attr_native_value = self.entity_description.end_value_fn(
self._attr_native_value
)
# keep value when program ends if no function is specified
elif current_status == StateStatus.PROGRAM_ENDED:
@@ -931,11 +927,11 @@ class MieleTimeSensor(MieleRestorableSensor):
# force unknown when appliance is not working (some devices are keeping last value until a new cycle starts)
elif current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE):
self._last_value = None
self._attr_native_value = None
# otherwise, cache value and return it
else:
self._last_value = current_value
self._attr_native_value = current_value
class MieleConsumptionSensor(MieleRestorableSensor):
@@ -943,13 +939,13 @@ class MieleConsumptionSensor(MieleRestorableSensor):
_is_reporting: bool = False
def _update_last_value(self) -> None:
def _update_native_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
current_status = StateStatus(self.device.state_status)
last_value = (
float(cast(str, self._last_value))
if self._last_value is not None and self._last_value != STATE_UNKNOWN
float(cast(str, self._attr_native_value))
if self._attr_native_value is not None
else 0
)
@@ -963,7 +959,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
StateStatus.SERVICE,
):
self._is_reporting = False
self._last_value = None
self._attr_native_value = None
# appliance might report the last value for consumption of previous cycle and it will report 0
# only after a while, so it is necessary to force 0 until we see the 0 value coming from API, unless
@@ -973,7 +969,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
and not self._is_reporting
and last_value > 0
):
self._last_value = current_value
self._attr_native_value = current_value
self._is_reporting = True
elif (
@@ -982,12 +978,12 @@ class MieleConsumptionSensor(MieleRestorableSensor):
and current_value is not None
and cast(int, current_value) > 0
):
self._last_value = 0
self._attr_native_value = 0
# keep value when program ends
elif current_status == StateStatus.PROGRAM_ENDED:
pass
else:
self._last_value = current_value
self._attr_native_value = current_value
self._is_reporting = True

View File

@@ -49,6 +49,44 @@ class NSConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
MINOR_VERSION = 1
async def _validate_api_key(self, api_key: str) -> dict[str, str]:
"""Validate the API key by testing connection to NS API.
Returns a dict of errors, empty if validation successful.
"""
errors: dict[str, str] = {}
client = NSAPI(api_key)
try:
await self.hass.async_add_executor_job(client.get_stations)
except HTTPError:
errors["base"] = "invalid_auth"
except (RequestsConnectionError, Timeout):
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception validating API key")
errors["base"] = "unknown"
return errors
def _is_api_key_already_configured(
self, api_key: str, exclude_entry_id: str | None = None
) -> dict[str, str]:
"""Check if the API key is already configured in another entry.
Args:
api_key: The API key to check.
exclude_entry_id: Optional entry ID to exclude from the check.
Returns:
A dict of errors, empty if not already configured.
"""
for entry in self._async_current_entries():
if (
entry.entry_id != exclude_entry_id
and entry.data.get(CONF_API_KEY) == api_key
):
return {"base": "already_configured"}
return {}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -56,16 +94,7 @@ class NSConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match(user_input)
client = NSAPI(user_input[CONF_API_KEY])
try:
await self.hass.async_add_executor_job(client.get_stations)
except HTTPError:
errors["base"] = "invalid_auth"
except (RequestsConnectionError, Timeout):
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception validating API key")
errors["base"] = "unknown"
errors = await self._validate_api_key(user_input[CONF_API_KEY])
if not errors:
return self.async_create_entry(
title=INTEGRATION_TITLE,
@@ -77,6 +106,33 @@ class NSConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration to update the API key from the UI."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
# Check if this API key is already used by another entry
errors = self._is_api_key_already_configured(
user_input[CONF_API_KEY], exclude_entry_id=reconfigure_entry.entry_id
)
if not errors:
errors = await self._validate_api_key(user_input[CONF_API_KEY])
if not errors:
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates={CONF_API_KEY: user_input[CONF_API_KEY]},
)
return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
errors=errors,
)
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Handle import from YAML configuration."""
self._async_abort_entries_match({CONF_API_KEY: import_data[CONF_API_KEY]})

View File

@@ -1,14 +1,25 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"already_configured": "This API key is already configured for another entry.",
"cannot_connect": "Could not connect to NS API. Check your API key.",
"invalid_auth": "[%key:common::config_flow::error::invalid_api_key%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reconfigure": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::nederlandse_spoorwegen::config::step::user::data_description::api_key%]"
},
"description": "Update your Nederlandse Spoorwegen API key."
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"

View File

@@ -7,6 +7,7 @@ import logging
from typing import TYPE_CHECKING, Any
from pynintendoparental import Authenticator
from pynintendoparental.api import Api
from pynintendoparental.exceptions import HttpException, InvalidSessionTokenException
import voluptuous as vol
@@ -14,7 +15,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_SESSION_TOKEN, DOMAIN
from .const import APP_SETUP_URL, CONF_SESSION_TOKEN, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -37,6 +38,9 @@ class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
)
if user_input is not None:
nintendo_api = Api(
self.auth, self.hass.config.time_zone, self.hass.config.language
)
try:
await self.auth.complete_login(
self.auth, user_input[CONF_API_TOKEN], False
@@ -48,12 +52,24 @@ class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
assert self.auth.account_id
await self.async_set_unique_id(self.auth.account_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=self.auth.account_id,
data={
CONF_SESSION_TOKEN: self.auth.get_session_token,
},
)
try:
if "base" not in errors:
await nintendo_api.async_get_account_devices()
except HttpException as err:
if err.status_code == 404:
return self.async_abort(
reason="no_devices_found",
description_placeholders={"more_info_url": APP_SETUP_URL},
)
errors["base"] = "cannot_connect"
else:
if "base" not in errors:
return self.async_create_entry(
title=self.auth.account_id,
data={
CONF_SESSION_TOKEN: self.auth.get_session_token,
},
)
return self.async_show_form(
step_id="user",
description_placeholders={"link": self.auth.login_url},

View File

@@ -8,4 +8,7 @@ BEDTIME_ALARM_MIN = "16:00"
BEDTIME_ALARM_MAX = "23:00"
BEDTIME_ALARM_DISABLE = "00:00"
APP_SETUP_URL = (
"https://www.nintendo.com/my/support/switch/parentalcontrols/app/setup.html"
)
ATTR_BONUS_TIME = "bonus_time"

View File

@@ -6,7 +6,10 @@ from datetime import timedelta
import logging
from pynintendoparental import Authenticator, NintendoParental
from pynintendoparental.exceptions import InvalidOAuthConfigurationException
from pynintendoparental.exceptions import (
InvalidOAuthConfigurationException,
NoDevicesFoundException,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -24,6 +27,8 @@ UPDATE_INTERVAL = timedelta(seconds=60)
class NintendoUpdateCoordinator(DataUpdateCoordinator[None]):
"""Nintendo data update coordinator."""
config_entry: NintendoParentalControlsConfigEntry
def __init__(
self,
hass: HomeAssistant,
@@ -50,3 +55,8 @@ class NintendoUpdateCoordinator(DataUpdateCoordinator[None]):
raise ConfigEntryError(
err, translation_domain=DOMAIN, translation_key="invalid_auth"
) from err
except NoDevicesFoundException as err:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="no_devices_found",
) from err

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["pynintendoparental"],
"quality_scale": "bronze",
"requirements": ["pynintendoparental==1.1.2"]
"requirements": ["pynintendoparental==1.1.3"]
}

View File

@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"no_devices_found": "There are no devices paired with this Nintendo account, go to [Nintendo Support]({more_info_url}) for further assistance.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
@@ -67,6 +68,9 @@
},
"device_not_found": {
"message": "Device not found."
},
"no_devices_found": {
"message": "No Nintendo devices found for this account."
}
},
"services": {

View File

@@ -14,7 +14,7 @@ from onedrive_personal_sdk.exceptions import (
NotFoundError,
OneDriveException,
)
from onedrive_personal_sdk.models.items import Item, ItemUpdate
from onedrive_personal_sdk.models.items import ItemUpdate
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
@@ -202,9 +202,7 @@ async def _get_onedrive_client(
)
async def _handle_item_operation(
func: Callable[[], Awaitable[Item]], folder: str
) -> Item:
async def _handle_item_operation[T](func: Callable[[], Awaitable[T]], folder: str) -> T:
try:
return await func()
except NotFoundError:

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.0.14"]
"requirements": ["onedrive-personal-sdk==0.0.15"]
}

View File

@@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["openai==2.2.0", "python-open-router==0.3.1"]
"requirements": ["openai==2.2.0", "python-open-router==0.3.2"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "bronze",
"requirements": ["opower==0.15.8"]
"requirements": ["opower==0.15.9"]
}

View File

@@ -229,7 +229,7 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the local authentication step via config flow."""
errors = {}
description_placeholders = {
"somfy-developer-mode-docs": "https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode#getting-started"
"somfy_developer_mode_docs": "https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode#getting-started"
}
if user_input:

View File

@@ -41,7 +41,7 @@
"token": "Token generated by the app used to control your device.",
"verify_ssl": "Verify the SSL certificate. Select this only if you are connecting via the hostname."
},
"description": "By activating the [Developer Mode of your TaHoma box]({somfy-developer-mode-docs}), you can authorize third-party software (like Home Assistant) to connect to it via your local network.\n\n1. Open the TaHoma By Somfy application on your device.\n2. Navigate to the Help & advanced features -> Advanced features menu in the application.\n3. Activate Developer Mode by tapping 7 times on the version number of your gateway (like 2025.1.4-11).\n4. Generate a token from the Developer Mode menu to authenticate your API calls.\n\n5. Enter the generated token below and update the host to include your Gateway PIN or the IP address of your gateway."
"description": "By activating the [Developer Mode of your TaHoma box]({somfy_developer_mode_docs}), you can authorize third-party software (like Home Assistant) to connect to it via your local network.\n\n1. Open the TaHoma By Somfy application on your device.\n2. Navigate to the Help & advanced features -> Advanced features menu in the application.\n3. Activate Developer Mode by tapping 7 times on the version number of your gateway (like 2025.1.4-11).\n4. Generate a token from the Developer Mode menu to authenticate your API calls.\n\n5. Enter the generated token below and update the host to include your Gateway PIN or the IP address of your gateway."
},
"local_or_cloud": {
"data": {

View File

@@ -23,6 +23,28 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR]
DATA_PRIVILEGED_KEY: HassKey[bool | None] = HassKey(DOMAIN)
async def async_migrate_entry(hass: HomeAssistant, entry: PingConfigEntry) -> bool:
"""Migrate old config entries."""
if entry.version == 1 and entry.minor_version == 1:
_LOGGER.debug("Migrating to minor version 2")
# Migrate device registry identifiers from homeassistant domain to ping domain
registry = dr.async_get(hass)
if (
device := registry.async_get_device(
identifiers={(HOMEASSISTANT_DOMAIN, entry.entry_id)}
)
) is not None and entry.entry_id in device.config_entries:
registry.async_update_device(
device_id=device.id,
new_identifiers={(DOMAIN, entry.entry_id)},
)
hass.config_entries.async_update_entry(entry, minor_version=2)
return True
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the ping integration."""
hass.data[DATA_PRIVILEGED_KEY] = await _can_use_icmp_lib_with_privilege()
@@ -32,19 +54,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: PingConfigEntry) -> bool:
"""Set up Ping (ICMP) from a config entry."""
# Migrate device registry identifiers from homeassistant domain to ping domain
registry = dr.async_get(hass)
if (
device := registry.async_get_device(
identifiers={(HOMEASSISTANT_DOMAIN, entry.entry_id)}
)
) is not None and entry.entry_id in device.config_entries:
registry.async_update_device(
device_id=device.id,
new_identifiers={(DOMAIN, entry.entry_id)},
)
privileged = hass.data[DATA_PRIVILEGED_KEY]
host: str = entry.options[CONF_HOST]

View File

@@ -37,6 +37,7 @@ class PingConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Ping."""
VERSION = 1
MINOR_VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None

View File

@@ -10,11 +10,12 @@ from homeassistant.components.device_tracker import (
ScannerEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .const import CONF_IMPORTED_BY
from .const import CONF_IMPORTED_BY, DOMAIN
from .coordinator import PingConfigEntry, PingUpdateCoordinator
@@ -24,7 +25,7 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Ping config entry."""
async_add_entities([PingDeviceTracker(entry, entry.runtime_data)])
async_add_entities([PingDeviceTracker(hass, entry, entry.runtime_data)])
class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity):
@@ -33,7 +34,10 @@ class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity)
_last_seen: datetime | None = None
def __init__(
self, config_entry: PingConfigEntry, coordinator: PingUpdateCoordinator
self,
hass: HomeAssistant,
config_entry: PingConfigEntry,
coordinator: PingUpdateCoordinator,
) -> None:
"""Initialize the Ping device tracker."""
super().__init__(coordinator)
@@ -46,6 +50,13 @@ class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity)
)
)
if (
device := dr.async_get(hass).async_get_device(
identifiers={(DOMAIN, config_entry.entry_id)}
)
) is not None:
self.device_entry = device
@property
def ip_address(self) -> str:
"""Return the primary ip address of the device."""

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from homeassistant.components.climate import (
@@ -13,18 +14,44 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
from .const import DOMAIN, MASTER_THERMOSTATS
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
from .entity import PlugwiseEntity
from .util import plugwise_command
ERROR_NO_SCHEDULE = "set_schedule_first"
PARALLEL_UPDATES = 0
@dataclass
class PlugwiseClimateExtraStoredData(ExtraStoredData):
"""Object to hold extra stored data."""
last_active_schedule: str | None
previous_action_mode: str | None
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the text data."""
return {
"last_active_schedule": self.last_active_schedule,
"previous_action_mode": self.previous_action_mode,
}
@classmethod
def from_dict(cls, restored: dict[str, Any]) -> PlugwiseClimateExtraStoredData:
"""Initialize a stored data object from a dict."""
return cls(
last_active_schedule=restored.get("last_active_schedule"),
previous_action_mode=restored.get("previous_action_mode"),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: PlugwiseConfigEntry,
@@ -56,14 +83,26 @@ async def async_setup_entry(
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
"""Representation of a Plugwise thermostat."""
_attr_name = None
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
_previous_mode: str = "heating"
_last_active_schedule: str | None = None
_previous_action_mode: str | None = HVACAction.HEATING.value
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added."""
await super().async_added_to_hass()
if extra_data := await self.async_get_last_extra_data():
plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict(
extra_data.as_dict()
)
self._last_active_schedule = plugwise_extra_data.last_active_schedule
self._previous_action_mode = plugwise_extra_data.previous_action_mode
def __init__(
self,
@@ -76,7 +115,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
gateway_id: str = coordinator.api.gateway_id
self._gateway_data = coordinator.data[gateway_id]
self._location = device_id
if (location := self.device.get("location")) is not None:
self._location = location
@@ -105,25 +143,19 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
self.device["thermostat"]["resolution"], 0.1
)
def _previous_action_mode(self, coordinator: PlugwiseDataUpdateCoordinator) -> None:
"""Return the previous action-mode when the regulation-mode is not heating or cooling.
Helper for set_hvac_mode().
"""
# When no cooling available, _previous_mode is always heating
if (
"regulation_modes" in self._gateway_data
and "cooling" in self._gateway_data["regulation_modes"]
):
mode = self._gateway_data["select_regulation_mode"]
if mode in ("cooling", "heating"):
self._previous_mode = mode
@property
def current_temperature(self) -> float:
"""Return the current temperature."""
return self.device["sensors"]["temperature"]
@property
def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
"""Return text specific state data to be restored."""
return PlugwiseClimateExtraStoredData(
last_active_schedule=self._last_active_schedule,
previous_action_mode=self._previous_action_mode,
)
@property
def target_temperature(self) -> float:
"""Return the temperature we try to reach.
@@ -170,9 +202,10 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
if self.coordinator.api.cooling_present:
if "regulation_modes" in self._gateway_data:
if self._gateway_data["select_regulation_mode"] == "cooling":
selected = self._gateway_data.get("select_regulation_mode")
if selected == HVACAction.COOLING.value:
hvac_modes.append(HVACMode.COOL)
if self._gateway_data["select_regulation_mode"] == "heating":
if selected == HVACAction.HEATING.value:
hvac_modes.append(HVACMode.HEAT)
else:
hvac_modes.append(HVACMode.HEAT_COOL)
@@ -184,8 +217,16 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
@property
def hvac_action(self) -> HVACAction:
"""Return the current running hvac operation if supported."""
# Keep track of the previous action-mode
self._previous_action_mode(self.coordinator)
# Keep track of the previous hvac_action mode.
# When no cooling available, _previous_action_mode is always heating
if (
"regulation_modes" in self._gateway_data
and HVACAction.COOLING.value in self._gateway_data["regulation_modes"]
):
mode = self._gateway_data["select_regulation_mode"]
if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
self._previous_action_mode = mode
if (action := self.device.get("control_state")) is not None:
return HVACAction(action)
@@ -219,14 +260,33 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
return
if hvac_mode == HVACMode.OFF:
await self.coordinator.api.set_regulation_mode(hvac_mode)
await self.coordinator.api.set_regulation_mode(hvac_mode.value)
else:
current = self.device.get("select_schedule")
desired = current
# Capture the last valid schedule
if desired and desired != "off":
self._last_active_schedule = desired
elif desired == "off":
desired = self._last_active_schedule
# Enabling HVACMode.AUTO requires a previously set schedule for saving and restoring
if hvac_mode == HVACMode.AUTO and not desired:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key=ERROR_NO_SCHEDULE,
)
await self.coordinator.api.set_schedule_state(
self._location,
"on" if hvac_mode == HVACMode.AUTO else "off",
STATE_ON if hvac_mode == HVACMode.AUTO else STATE_OFF,
desired,
)
if self.hvac_mode == HVACMode.OFF:
await self.coordinator.api.set_regulation_mode(self._previous_mode)
if self.hvac_mode == HVACMode.OFF and self._previous_action_mode:
await self.coordinator.api.set_regulation_mode(
self._previous_action_mode
)
@plugwise_command
async def async_set_preset_mode(self, preset_mode: str) -> None:

View File

@@ -8,6 +8,6 @@
"iot_class": "local_polling",
"loggers": ["plugwise"],
"quality_scale": "platinum",
"requirements": ["plugwise==1.8.2"],
"requirements": ["plugwise==1.8.3"],
"zeroconf": ["_plugwise._tcp.local."]
}

View File

@@ -314,6 +314,9 @@
"invalid_xml_data": {
"message": "[%key:component::plugwise::config::error::response_error%]"
},
"set_schedule_first": {
"message": "Failed setting HVACMode, set a schedule first."
},
"unsupported_firmware": {
"message": "[%key:component::plugwise::config::error::unsupported%]"
}

View File

@@ -38,9 +38,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
client = Portainer(
api_url=data[CONF_URL],
api_key=data[CONF_API_TOKEN],
session=async_get_clientsession(
hass=hass, verify_ssl=data.get(CONF_VERIFY_SSL, True)
),
session=async_get_clientsession(hass=hass, verify_ssl=data[CONF_VERIFY_SSL]),
)
try:
await client.get_endpoints()

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyportainer==1.0.11"]
"requirements": ["pyportainer==1.0.12"]
}

View File

@@ -19,5 +19,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.16.2"]
"requirements": ["reolink-aio==0.16.4"]
}

View File

@@ -835,6 +835,7 @@
"vehicle_type": {
"name": "Vehicle type",
"state": {
"bus": "Bus",
"motorcycle": "Motorcycle",
"pickup_truck": "Pickup truck",
"sedan": "Sedan",

View File

@@ -16,5 +16,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/ruuvitag_ble",
"iot_class": "local_push",
"requirements": ["ruuvitag-ble==0.2.1"]
"requirements": ["ruuvitag-ble==0.3.0"]
}

View File

@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING
from sfrbox_api.bridge import SFRBox
from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
@@ -16,11 +15,10 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, PLATFORMS, PLATFORMS_WITH_AUTH
from .coordinator import SFRDataUpdateCoordinator
from .models import DomainData
from .coordinator import SFRConfigEntry, SFRDataUpdateCoordinator, SFRRuntimeData
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SFRConfigEntry) -> bool:
"""Set up SFR box as config entry."""
box = SFRBox(ip=entry.data[CONF_HOST], client=async_get_clientsession(hass))
platforms = PLATFORMS
@@ -35,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise ConfigEntryNotReady from err
platforms = PLATFORMS_WITH_AUTH
data = DomainData(
data = SFRRuntimeData(
box=box,
dsl=SFRDataUpdateCoordinator(
hass, entry, box, "dsl", lambda b: b.dsl_get_info()
@@ -64,8 +62,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
tasks.append(data.ftth.async_config_entry_first_refresh())
await asyncio.gather(*tasks)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
@@ -77,13 +73,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
configuration_url=f"http://{entry.data[CONF_HOST]}",
)
entry.runtime_data = data
await hass.config_entries.async_forward_entry_setups(entry, platforms)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: SFRConfigEntry) -> 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
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -6,23 +6,19 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo
from sfrbox_api.models import DslInfo, FtthInfo, WanInfo
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import SFRDataUpdateCoordinator
from .models import DomainData
from .coordinator import SFRConfigEntry
from .entity import SFRCoordinatorEntity
@dataclass(frozen=True, kw_only=True)
@@ -63,11 +59,11 @@ WAN_SENSOR_TYPES: tuple[SFRBoxBinarySensorEntityDescription[WanInfo], ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SFRConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sensors."""
data: DomainData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
system_info = data.system.data
if TYPE_CHECKING:
assert system_info is not None
@@ -90,29 +86,10 @@ async def async_setup_entry(
async_add_entities(entities)
class SFRBoxBinarySensor[_T](
CoordinatorEntity[SFRDataUpdateCoordinator[_T]], BinarySensorEntity
):
"""SFR Box sensor."""
class SFRBoxBinarySensor[_T](SFRCoordinatorEntity[_T], BinarySensorEntity):
"""SFR Box binary sensor."""
entity_description: SFRBoxBinarySensorEntityDescription[_T]
_attr_has_entity_name = True
def __init__(
self,
coordinator: SFRDataUpdateCoordinator[_T],
description: SFRBoxBinarySensorEntityDescription,
system_info: SystemInfo,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = (
f"{system_info.mac_addr}_{coordinator.name}_{description.key}"
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, system_info.mac_addr)},
)
@property
def is_on(self) -> bool | None:

View File

@@ -16,15 +16,13 @@ from homeassistant.components.button import (
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .models import DomainData
from .coordinator import SFRConfigEntry
from .entity import SFREntity
def with_error_wrapping[**_P, _R](
@@ -66,11 +64,11 @@ BUTTON_TYPES: tuple[SFRBoxButtonEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SFRConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the buttons."""
data: DomainData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
system_info = data.system.data
if TYPE_CHECKING:
assert system_info is not None
@@ -81,11 +79,10 @@ async def async_setup_entry(
async_add_entities(entities)
class SFRBoxButton(ButtonEntity):
"""Mixin for button specific attributes."""
class SFRBoxButton(SFREntity, ButtonEntity):
"""SFR Box button."""
entity_description: SFRBoxButtonEntityDescription
_attr_has_entity_name = True
def __init__(
self,
@@ -93,13 +90,9 @@ class SFRBoxButton(ButtonEntity):
description: SFRBoxButtonEntityDescription,
system_info: SystemInfo,
) -> None:
"""Initialize the sensor."""
self.entity_description = description
"""Initialize the button."""
super().__init__(description, system_info)
self._box = box
self._attr_unique_id = f"{system_info.mac_addr}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, system_info.mac_addr)},
)
@with_error_wrapping
async def async_press(self) -> None:

View File

@@ -1,12 +1,16 @@
"""SFR Box coordinator."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any
from sfrbox_api.bridge import SFRBox
from sfrbox_api.exceptions import SFRBoxError
from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -15,16 +19,29 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
_LOGGER = logging.getLogger(__name__)
_SCAN_INTERVAL = timedelta(minutes=1)
type SFRConfigEntry = ConfigEntry[SFRRuntimeData]
@dataclass
class SFRRuntimeData:
"""Runtime data for SFR Box."""
box: SFRBox
dsl: SFRDataUpdateCoordinator[DslInfo]
ftth: SFRDataUpdateCoordinator[FtthInfo]
system: SFRDataUpdateCoordinator[SystemInfo]
wan: SFRDataUpdateCoordinator[WanInfo]
class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT | None]):
"""Coordinator to manage data updates."""
config_entry: ConfigEntry
config_entry: SFRConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: SFRConfigEntry,
box: SFRBox,
name: str,
method: Callable[[SFRBox], Coroutine[Any, Any, _DataT | None]],

View File

@@ -6,11 +6,9 @@ import dataclasses
from typing import TYPE_CHECKING, Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .models import DomainData
from .coordinator import SFRConfigEntry
if TYPE_CHECKING:
from _typeshed import DataclassInstance
@@ -25,10 +23,10 @@ def _async_redact_data(obj: DataclassInstance | None) -> dict[str, Any] | None:
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: SFRConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data: DomainData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
return {
"entry": {

View File

@@ -0,0 +1,45 @@
"""SFR Box base entity."""
from __future__ import annotations
from sfrbox_api.models import SystemInfo
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import SFRDataUpdateCoordinator
class SFREntity(Entity):
"""SFR Box entity."""
_attr_has_entity_name = True
def __init__(self, description: EntityDescription, system_info: SystemInfo) -> None:
"""Initialize the entity."""
self.entity_description = description
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, system_info.mac_addr)},
)
self._attr_unique_id = f"{system_info.mac_addr}_{description.key}"
class SFRCoordinatorEntity[_T](
CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SFREntity
):
"""SFR Box coordinator entity."""
def __init__(
self,
coordinator: SFRDataUpdateCoordinator[_T],
description: EntityDescription,
system_info: SystemInfo,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
SFREntity.__init__(self, description, system_info)
self._attr_unique_id = (
f"{system_info.mac_addr}_{coordinator.name}_{description.key}"
)

View File

@@ -1,19 +0,0 @@
"""SFR Box models."""
from dataclasses import dataclass
from sfrbox_api.bridge import SFRBox
from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo
from .coordinator import SFRDataUpdateCoordinator
@dataclass
class DomainData:
"""Domain data for SFR Box."""
box: SFRBox
dsl: SFRDataUpdateCoordinator[DslInfo]
ftth: SFRDataUpdateCoordinator[FtthInfo]
system: SFRDataUpdateCoordinator[SystemInfo]
wan: SFRDataUpdateCoordinator[WanInfo]

View File

@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS,
EntityCategory,
@@ -21,14 +20,11 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import SFRDataUpdateCoordinator
from .models import DomainData
from .coordinator import SFRConfigEntry
from .entity import SFRCoordinatorEntity
@dataclass(frozen=True, kw_only=True)
@@ -220,11 +216,11 @@ def _get_temperature(value: float | None) -> float | None:
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SFRConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sensors."""
data: DomainData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
system_info = data.system.data
if TYPE_CHECKING:
assert system_info is not None
@@ -246,27 +242,10 @@ async def async_setup_entry(
async_add_entities(entities)
class SFRBoxSensor[_T](CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SensorEntity):
class SFRBoxSensor[_T](SFRCoordinatorEntity[_T], SensorEntity):
"""SFR Box sensor."""
entity_description: SFRBoxSensorEntityDescription[_T]
_attr_has_entity_name = True
def __init__(
self,
coordinator: SFRDataUpdateCoordinator[_T],
description: SFRBoxSensorEntityDescription,
system_info: SystemInfo,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = (
f"{system_info.mac_addr}_{coordinator.name}_{description.key}"
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, system_info.mac_addr)},
)
@property
def native_value(self) -> StateType:

View File

@@ -13,6 +13,10 @@
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"password": "The password for accessing your SFR box's web interface, the default is the WiFi security key found on the device label",
"username": "The username for accessing your SFR box's web interface, the default is 'admin'"
}
},
"choose_auth": {

View File

@@ -417,7 +417,7 @@ def get_rpc_sub_device_name(
"""Get name based on device and channel name."""
if key in device.config and key != "em:0":
# workaround for Pro 3EM, we don't want to get name for em:0
if (zone_id := get_irrigation_zone_id(device.config, key)) is not None:
if (zone_id := get_irrigation_zone_id(device, key)) is not None:
# workaround for Irrigation controller, name stored in "service:0"
if zone_name := device.config["service:0"]["zones"][zone_id]["name"]:
return cast(str, zone_name)
@@ -792,9 +792,13 @@ async def get_rpc_scripts_event_types(
return script_events
def get_irrigation_zone_id(config: dict[str, Any], key: str) -> int | None:
def get_irrigation_zone_id(device: RpcDevice, key: str) -> int | None:
"""Return the zone id if the component is an irrigation zone."""
if key in config and (zone := get_rpc_role_by_key(config, key)).startswith("zone"):
if (
device.initialized
and key in device.config
and (zone := get_rpc_role_by_key(device.config, key)).startswith("zone")
):
return int(zone[4:])
return None
@@ -837,7 +841,7 @@ def get_rpc_device_info(
if (
(
component not in (*All_LIGHT_TYPES, "cover", "em1", "switch")
and get_irrigation_zone_id(device.config, key) is None
and get_irrigation_zone_id(device, key) is None
)
or idx is None
or len(get_rpc_key_instances(device.status, component, all_lights=True)) < 2

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/smarttub",
"iot_class": "cloud_polling",
"loggers": ["smarttub"],
"requirements": ["python-smarttub==0.0.44"]
"requirements": ["python-smarttub==0.0.45"]
}

View File

@@ -4,11 +4,13 @@ import logging
from libsoundtouch import soundtouch_device
from libsoundtouch.device import SoundTouchDevice
import requests
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
@@ -130,7 +132,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Bose SoundTouch from a config entry."""
device = await hass.async_add_executor_job(soundtouch_device, entry.data[CONF_HOST])
try:
device = await hass.async_add_executor_job(
soundtouch_device, entry.data[CONF_HOST]
)
except requests.exceptions.ConnectionError as err:
raise ConfigEntryNotReady(
f"Unable to connect to SoundTouch device at {entry.data[CONF_HOST]}"
) from err
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SoundTouchData(device)

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