mirror of
https://github.com/home-assistant/core.git
synced 2025-11-06 17:40:11 +00:00
Compare commits
103 Commits
matter-err
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b91a92554 | ||
|
|
7855df92c8 | ||
|
|
11309f89f0 | ||
|
|
396a987035 | ||
|
|
b7696bfb20 | ||
|
|
5cfbe2cf71 | ||
|
|
4e255286af | ||
|
|
53a96af844 | ||
|
|
accb705d8b | ||
|
|
1793abce4f | ||
|
|
8bfed0b60c | ||
|
|
016c1de2ef | ||
|
|
c270f31365 | ||
|
|
f9e06acfc7 | ||
|
|
901558b293 | ||
|
|
c09cf36345 | ||
|
|
926627b49c | ||
|
|
a8eeba9c5f | ||
|
|
e4591c27c0 | ||
|
|
40dedec602 | ||
|
|
3a65b5ca70 | ||
|
|
dbeb82861f | ||
|
|
e43c35ab2d | ||
|
|
b4c4fdefe3 | ||
|
|
965dd7c557 | ||
|
|
9a921f2c8e | ||
|
|
aaae3244a8 | ||
|
|
40ff100900 | ||
|
|
1b62b2309f | ||
|
|
9d57251aea | ||
|
|
f877614e7f | ||
|
|
170e1e87c7 | ||
|
|
e1feba5c86 | ||
|
|
9bf52b7966 | ||
|
|
3bc61a3564 | ||
|
|
d2ba94e1bf | ||
|
|
9a4ed82399 | ||
|
|
b5136d01aa | ||
|
|
d3e05090ea | ||
|
|
7e75ca7af9 | ||
|
|
6616b5775f | ||
|
|
69b82d4c59 | ||
|
|
6b9709677a | ||
|
|
a4e9c82c84 | ||
|
|
de86bedb80 | ||
|
|
9111c6df90 | ||
|
|
751f6bddb1 | ||
|
|
c9a61de0a1 | ||
|
|
01fb46d903 | ||
|
|
d26f61c9fe | ||
|
|
a47a144312 | ||
|
|
69cf4f99d1 | ||
|
|
e6c757c187 | ||
|
|
a36b0e2f3f | ||
|
|
1a7c6cd96c | ||
|
|
ba3e538402 | ||
|
|
b2cd08aa65 | ||
|
|
06dcd25a16 | ||
|
|
fd36782bae | ||
|
|
ed4573db57 | ||
|
|
78373a6483 | ||
|
|
8455c35bec | ||
|
|
00887a2f3f | ||
|
|
f1ca7543fa | ||
|
|
bb72b24ba9 | ||
|
|
322a27d992 | ||
|
|
a3b516110b | ||
|
|
95ac5c0183 | ||
|
|
40995b6d32 | ||
|
|
f64c029bd1 | ||
|
|
a050c0cd05 | ||
|
|
0c121468e0 | ||
|
|
af277651f8 | ||
|
|
ca90826478 | ||
|
|
dd4789af4e | ||
|
|
ca49b6e7e2 | ||
|
|
fdbe293483 | ||
|
|
aa67b46f6f | ||
|
|
9f21a97d39 | ||
|
|
49158fad48 | ||
|
|
dff8e5221b | ||
|
|
dbfa0aa22c | ||
|
|
ec4464d65f | ||
|
|
6c8dffd521 | ||
|
|
09763012fc | ||
|
|
aa5b970102 | ||
|
|
80912045d7 | ||
|
|
dbda31f6d5 | ||
|
|
24219dd8f7 | ||
|
|
77d0cf1573 | ||
|
|
1ea534e400 | ||
|
|
a6ba8fa69c | ||
|
|
a6c1ce86d8 | ||
|
|
fbb07e16cb | ||
|
|
1387308f48 | ||
|
|
48d371eddb | ||
|
|
c45c11574c | ||
|
|
14eb103338 | ||
|
|
2bdb258a39 | ||
|
|
5b323526b6 | ||
|
|
9d434c9403 | ||
|
|
e4103137ef | ||
|
|
d9d4cc9004 |
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -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
2
Dockerfile
generated
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -26,3 +26,6 @@ COUNTRY_DOMAINS = {
|
||||
"us": DEFAULT_DOMAIN,
|
||||
"za": "co.za",
|
||||
}
|
||||
|
||||
CATEGORY_SENSORS = "sensors"
|
||||
CATEGORY_NOTIFICATIONS = "notifications"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==6.4.6"]
|
||||
"requirements": ["aioamazondevices==6.5.6"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pycync==0.4.2"]
|
||||
"requirements": ["pycync==0.4.3"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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." }
|
||||
]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Virtual integration: Enmax Energy."""
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"domain": "enmax",
|
||||
"name": "Enmax Energy",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "opower"
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"default": "mdi:test-tube"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"name": "Beta firmware updates"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"device_disconnected": {
|
||||
"message": "The device is not plugged in"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
64
homeassistant/components/homeassistant_hardware/switch.py
Normal file
64
homeassistant/components/homeassistant_hardware/switch.py
Normal 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()
|
||||
@@ -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."""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"default": "mdi:test-tube"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"name": "Beta firmware updates"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"device_disconnected": {
|
||||
"message": "The device is not plugged in"
|
||||
|
||||
57
homeassistant/components/homeassistant_sky_connect/switch.py
Normal file
57
homeassistant/components/homeassistant_sky_connect/switch.py
Normal 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,
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
9
homeassistant/components/homeassistant_yellow/icons.json
Normal file
9
homeassistant/components/homeassistant_yellow/icons.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"default": "mdi:test-tube"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"name": "Radio beta firmware updates"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"radio_firmware": {
|
||||
"name": "Radio firmware"
|
||||
|
||||
50
homeassistant/components/homeassistant_yellow/switch.py
Normal file
50
homeassistant/components/homeassistant_yellow/switch.py
Normal 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,
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]}/",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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(",", ".")
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pylitterbot==2024.2.7"]
|
||||
"requirements": ["pylitterbot==2025.0.0"]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]})
|
||||
|
||||
@@ -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%]"
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynintendoparental"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynintendoparental==1.1.2"]
|
||||
"requirements": ["pynintendoparental==1.1.3"]
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.15.8"]
|
||||
"requirements": ["opower==0.15.9"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyportainer==1.0.11"]
|
||||
"requirements": ["pyportainer==1.0.12"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -835,6 +835,7 @@
|
||||
"vehicle_type": {
|
||||
"name": "Vehicle type",
|
||||
"state": {
|
||||
"bus": "Bus",
|
||||
"motorcycle": "Motorcycle",
|
||||
"pickup_truck": "Pickup truck",
|
||||
"sedan": "Sedan",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]],
|
||||
|
||||
@@ -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": {
|
||||
|
||||
45
homeassistant/components/sfr_box/entity.py
Normal file
45
homeassistant/components/sfr_box/entity.py
Normal 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}"
|
||||
)
|
||||
@@ -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]
|
||||
@@ -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:
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user