mirror of
https://github.com/home-assistant/core.git
synced 2026-06-29 14:26:24 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb68146bce | |||
| 072c570660 | |||
| be62023040 | |||
| b9c563538a | |||
| 0e78002c4a | |||
| e921373833 | |||
| ccc7eec253 |
Generated
+2
-2
@@ -1169,8 +1169,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ping/ @jpbede
|
||||
/homeassistant/components/plaato/ @JohNan
|
||||
/tests/components/plaato/ @JohNan
|
||||
/homeassistant/components/playstation_network/ @jackjpowell @tr4nt0r
|
||||
/tests/components/playstation_network/ @jackjpowell @tr4nt0r
|
||||
/homeassistant/components/playstation_network/ @jackjpowell
|
||||
/tests/components/playstation_network/ @jackjpowell
|
||||
/homeassistant/components/plex/ @jjlawren
|
||||
/tests/components/plex/ @jjlawren
|
||||
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
|
||||
|
||||
@@ -89,7 +89,6 @@ from .helpers import (
|
||||
restore_state,
|
||||
template,
|
||||
translation,
|
||||
trigger,
|
||||
)
|
||||
from .helpers.dispatcher import async_dispatcher_send_internal
|
||||
from .helpers.storage import get_internal_store_manager
|
||||
@@ -453,7 +452,6 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||
create_eager_task(restore_state.async_load(hass)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -185,7 +185,6 @@ FORECAST_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||
keys=[AOD_TOWN, AOD_FORECAST_DAILY, AOD_FORECAST_CURRENT, AOD_WIND_DIRECTION],
|
||||
name="Daily forecast wind bearing",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
AemetSensorEntityDescription(
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -193,7 +192,6 @@ FORECAST_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_FORECAST_CURRENT, AOD_WIND_DIRECTION],
|
||||
name="Hourly forecast wind bearing",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
AemetSensorEntityDescription(
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -337,7 +335,6 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||
name="Wind bearing",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
AemetSensorEntityDescription(
|
||||
key=ATTR_API_WIND_MAX_SPEED,
|
||||
|
||||
@@ -71,7 +71,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
data: dict[str, Any] = {}
|
||||
data = {}
|
||||
try:
|
||||
obs = await self.airnow.observations.latLong(
|
||||
self.latitude,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airnow",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyairnow"],
|
||||
"requirements": ["pyairnow==1.3.1"]
|
||||
"requirements": ["pyairnow==1.2.1"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
from aioamazondevices.const import SENSOR_STATE_OFF
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@@ -29,8 +28,7 @@ PARALLEL_UPDATES = 0
|
||||
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Alexa Devices binary sensor entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice, str], bool]
|
||||
is_supported: Callable[[AmazonDevice, str], bool] = lambda device, key: True
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
|
||||
|
||||
BINARY_SENSORS: Final = (
|
||||
@@ -38,49 +36,13 @@ BINARY_SENSORS: Final = (
|
||||
key="online",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
is_on_fn=lambda device, _: device.online,
|
||||
is_on_fn=lambda _device: _device.online,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="bluetooth",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
translation_key="bluetooth",
|
||||
is_on_fn=lambda device, _: device.bluetooth_state,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="babyCryDetectionState",
|
||||
translation_key="baby_cry_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="beepingApplianceDetectionState",
|
||||
translation_key="beeping_appliance_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="coughDetectionState",
|
||||
translation_key="cough_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="dogBarkDetectionState",
|
||||
translation_key="dog_bark_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="humanPresenceDetectionState",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="waterSoundsDetectionState",
|
||||
translation_key="water_sounds_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
is_on_fn=lambda _device: _device.bluetooth_state,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -98,7 +60,6 @@ async def async_setup_entry(
|
||||
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in BINARY_SENSORS
|
||||
for serial_num in coordinator.data
|
||||
if sensor_desc.is_supported(coordinator.data[serial_num], sensor_desc.key)
|
||||
)
|
||||
|
||||
|
||||
@@ -110,6 +71,4 @@ class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self.entity_description.is_on_fn(
|
||||
self.device, self.entity_description.key
|
||||
)
|
||||
return self.entity_description.is_on_fn(self.device)
|
||||
|
||||
@@ -2,39 +2,9 @@
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"bluetooth": {
|
||||
"default": "mdi:bluetooth-off",
|
||||
"default": "mdi:bluetooth",
|
||||
"state": {
|
||||
"on": "mdi:bluetooth"
|
||||
}
|
||||
},
|
||||
"baby_cry_detection": {
|
||||
"default": "mdi:account-voice-off",
|
||||
"state": {
|
||||
"on": "mdi:account-voice"
|
||||
}
|
||||
},
|
||||
"beeping_appliance_detection": {
|
||||
"default": "mdi:bell-off",
|
||||
"state": {
|
||||
"on": "mdi:bell-ring"
|
||||
}
|
||||
},
|
||||
"cough_detection": {
|
||||
"default": "mdi:blur-off",
|
||||
"state": {
|
||||
"on": "mdi:blur"
|
||||
}
|
||||
},
|
||||
"dog_bark_detection": {
|
||||
"default": "mdi:dog-side-off",
|
||||
"state": {
|
||||
"on": "mdi:dog-side"
|
||||
}
|
||||
},
|
||||
"water_sounds_detection": {
|
||||
"default": "mdi:water-pump-off",
|
||||
"state": {
|
||||
"on": "mdi:water-pump"
|
||||
"off": "mdi:bluetooth-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.1.19"]
|
||||
"requirements": ["aioamazondevices==3.1.14"]
|
||||
}
|
||||
|
||||
@@ -41,21 +41,6 @@
|
||||
"binary_sensor": {
|
||||
"bluetooth": {
|
||||
"name": "Bluetooth"
|
||||
},
|
||||
"baby_cry_detection": {
|
||||
"name": "Baby crying"
|
||||
},
|
||||
"beeping_appliance_detection": {
|
||||
"name": "Beeping appliance"
|
||||
},
|
||||
"cough_detection": {
|
||||
"name": "Coughing"
|
||||
},
|
||||
"dog_bark_detection": {
|
||||
"name": "Dog barking"
|
||||
},
|
||||
"water_sounds_detection": {
|
||||
"name": "Water sounds"
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
|
||||
@@ -260,18 +260,11 @@ class APIEntityStateView(HomeAssistantView):
|
||||
if not user.is_admin:
|
||||
raise Unauthorized(entity_id=entity_id)
|
||||
hass = request.app[KEY_HASS]
|
||||
|
||||
body = await request.text()
|
||||
|
||||
try:
|
||||
data: Any = json_loads(body) if body else None
|
||||
data = await request.json()
|
||||
except ValueError:
|
||||
return self.json_message("Invalid JSON specified.", HTTPStatus.BAD_REQUEST)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return self.json_message(
|
||||
"State data should be a JSON object.", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
if (new_state := data.get("state")) is None:
|
||||
return self.json_message("No state specified.", HTTPStatus.BAD_REQUEST)
|
||||
|
||||
@@ -484,19 +477,9 @@ class APITemplateView(HomeAssistantView):
|
||||
@require_admin
|
||||
async def post(self, request: web.Request) -> web.Response:
|
||||
"""Render a template."""
|
||||
body = await request.text()
|
||||
|
||||
try:
|
||||
data: Any = json_loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message("Invalid JSON specified.", HTTPStatus.BAD_REQUEST)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return self.json_message(
|
||||
"Template data should be a JSON object.", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
tpl = _cached_template(data["template"], request.app[KEY_HASS])
|
||||
try:
|
||||
data = await request.json()
|
||||
tpl = _cached_template(data["template"], request.app[KEY_HASS])
|
||||
return tpl.async_render(variables=data.get("variables"), parse_result=False) # type: ignore[no-any-return]
|
||||
except (ValueError, TemplateError) as ex:
|
||||
return self.json_message(
|
||||
|
||||
@@ -1119,7 +1119,6 @@ class PipelineRun:
|
||||
) is not None:
|
||||
# Sentence trigger matched
|
||||
agent_id = "sentence_trigger"
|
||||
processed_locally = True
|
||||
intent_response = intent.IntentResponse(
|
||||
self.pipeline.conversation_language
|
||||
)
|
||||
|
||||
@@ -86,17 +86,3 @@ ask_question:
|
||||
required: false
|
||||
selector:
|
||||
object:
|
||||
label_field: sentences
|
||||
description_field: id
|
||||
multiple: true
|
||||
translation_key: answers
|
||||
fields:
|
||||
id:
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
sentences:
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
multiple: true
|
||||
|
||||
@@ -90,13 +90,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"answers": {
|
||||
"fields": {
|
||||
"id": "Answer ID",
|
||||
"sentences": "Sentences"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.104.0"],
|
||||
"requirements": ["hass-nabucasa==0.103.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -336,7 +336,13 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
"" if unit is None else unit
|
||||
)
|
||||
|
||||
self._prune_state_list(new_state.last_reported)
|
||||
# filter out all derivatives older than `time_window` from our window list
|
||||
self._state_list = [
|
||||
(time_start, time_end, state)
|
||||
for time_start, time_end, state in self._state_list
|
||||
if (new_state.last_reported - time_end).total_seconds()
|
||||
< self._time_window
|
||||
]
|
||||
|
||||
try:
|
||||
elapsed_time = (
|
||||
@@ -374,14 +380,25 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
(old_last_reported, new_state.last_reported, new_derivative)
|
||||
)
|
||||
|
||||
def calculate_weight(
|
||||
start: datetime, end: datetime, now: datetime
|
||||
) -> float:
|
||||
window_start = now - timedelta(seconds=self._time_window)
|
||||
if start < window_start:
|
||||
weight = (end - window_start).total_seconds() / self._time_window
|
||||
else:
|
||||
weight = (end - start).total_seconds() / self._time_window
|
||||
return weight
|
||||
|
||||
# If outside of time window just report derivative (is the same as modeling it in the window),
|
||||
# otherwise take the weighted average with the previous derivatives
|
||||
if elapsed_time > self._time_window:
|
||||
derivative = new_derivative
|
||||
else:
|
||||
derivative = self._calc_derivative_from_state_list(
|
||||
new_state.last_reported
|
||||
)
|
||||
derivative = Decimal("0.00")
|
||||
for start, end, value in self._state_list:
|
||||
weight = calculate_weight(start, end, new_state.last_reported)
|
||||
derivative = derivative + (value * Decimal(weight))
|
||||
self._attr_native_value = round(derivative, self._round_digits)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from homeassistant.const import CONF_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
ConditionCheckerType,
|
||||
trace_condition_function,
|
||||
)
|
||||
@@ -52,38 +51,20 @@ class DeviceAutomationConditionProtocol(Protocol):
|
||||
"""List conditions."""
|
||||
|
||||
|
||||
class DeviceCondition(Condition):
|
||||
"""Device condition."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Initialize condition."""
|
||||
self._config = config
|
||||
self._hass = hass
|
||||
|
||||
@classmethod
|
||||
async def async_validate_condition_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate device condition config."""
|
||||
return await async_validate_device_automation_config(
|
||||
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
|
||||
)
|
||||
|
||||
async def async_condition_from_config(self) -> condition.ConditionCheckerType:
|
||||
"""Test a device condition."""
|
||||
platform = await async_get_device_automation_platform(
|
||||
self._hass, self._config[CONF_DOMAIN], DeviceAutomationType.CONDITION
|
||||
)
|
||||
return trace_condition_function(
|
||||
platform.async_condition_from_config(self._hass, self._config)
|
||||
)
|
||||
async def async_validate_condition_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate device condition config."""
|
||||
return await async_validate_device_automation_config(
|
||||
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
|
||||
)
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"device": DeviceCondition,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the sun conditions."""
|
||||
return CONDITIONS
|
||||
async def async_condition_from_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> condition.ConditionCheckerType:
|
||||
"""Test a device condition."""
|
||||
platform = await async_get_device_automation_platform(
|
||||
hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION
|
||||
)
|
||||
return trace_condition_function(platform.async_condition_from_config(hass, config))
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeControlConfigEntry
|
||||
from .entity import DevoloMultiLevelSwitchDeviceEntity
|
||||
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeControlConfigEntry
|
||||
from .entity import DevoloMultiLevelSwitchDeviceEntity
|
||||
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Base class for multi level switches in devolo Home Control."""
|
||||
|
||||
from devolo_home_control_api.devices.zwave import Zwave
|
||||
from devolo_home_control_api.homecontrol import HomeControl
|
||||
|
||||
from .entity import DevoloDeviceEntity
|
||||
|
||||
|
||||
class DevoloMultiLevelSwitchDeviceEntity(DevoloDeviceEntity):
|
||||
"""Representation of a multi level switch device within devolo Home Control. Something like a dimmer or a thermostat."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str
|
||||
) -> None:
|
||||
"""Initialize a multi level switch within devolo Home Control."""
|
||||
super().__init__(
|
||||
homecontrol=homecontrol,
|
||||
device_instance=device_instance,
|
||||
element_uid=element_uid,
|
||||
)
|
||||
self._multi_level_switch_property = device_instance.multi_level_switch_property[
|
||||
element_uid
|
||||
]
|
||||
|
||||
self._value = self._multi_level_switch_property.value
|
||||
@@ -90,24 +90,3 @@ class DevoloDeviceEntity(Entity):
|
||||
self._attr_available = self._device_instance.is_online()
|
||||
else:
|
||||
_LOGGER.debug("No valid message received: %s", message)
|
||||
|
||||
|
||||
class DevoloMultiLevelSwitchDeviceEntity(DevoloDeviceEntity):
|
||||
"""Representation of a multi level switch device within devolo Home Control. Something like a dimmer or a thermostat."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str
|
||||
) -> None:
|
||||
"""Initialize a multi level switch within devolo Home Control."""
|
||||
super().__init__(
|
||||
homecontrol=homecontrol,
|
||||
device_instance=device_instance,
|
||||
element_uid=element_uid,
|
||||
)
|
||||
self._multi_level_switch_property = device_instance.multi_level_switch_property[
|
||||
element_uid
|
||||
]
|
||||
|
||||
self._value = self._multi_level_switch_property.value
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeControlConfigEntry
|
||||
from .entity import DevoloMultiLevelSwitchDeviceEntity
|
||||
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeControlConfigEntry
|
||||
from .entity import DevoloMultiLevelSwitchDeviceEntity
|
||||
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -19,16 +19,6 @@
|
||||
"password": "Password of your mydevolo account."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"username": "[%key:component::devolo_home_control::config::step::user::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::devolo_home_control::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::devolo_home_control::config::step::user::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"data": {
|
||||
"username": "[%key:component::devolo_home_control::config::step::user::data::username%]",
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dormakaba_dkey",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["py-dormakaba-dkey==1.0.6"]
|
||||
"requirements": ["py-dormakaba-dkey==1.0.5"]
|
||||
}
|
||||
|
||||
@@ -284,15 +284,11 @@ class EsphomeAssistSatellite(
|
||||
assert event.data is not None
|
||||
data_to_send = {"text": event.data["stt_output"]["text"]}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS:
|
||||
if (
|
||||
not event.data
|
||||
or ("tts_start_streaming" not in event.data)
|
||||
or (not event.data["tts_start_streaming"])
|
||||
):
|
||||
# ESPHome only needs to know if early TTS streaming is available
|
||||
return
|
||||
|
||||
data_to_send = {"tts_start_streaming": "1"}
|
||||
data_to_send = {
|
||||
"tts_start_streaming": "1"
|
||||
if (event.data and event.data.get("tts_start_streaming"))
|
||||
else "0",
|
||||
}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END:
|
||||
assert event.data is not None
|
||||
data_to_send = {
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
import functools
|
||||
import logging
|
||||
import math
|
||||
from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeVar, cast
|
||||
|
||||
@@ -14,6 +13,7 @@ from aioesphomeapi import (
|
||||
EntityCategory as EsphomeEntityCategory,
|
||||
EntityInfo,
|
||||
EntityState,
|
||||
build_unique_id,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -24,7 +24,6 @@ from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_platform,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -33,11 +32,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from .const import DOMAIN
|
||||
|
||||
# Import config flow so that it's added to the registry
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData, build_device_unique_id
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||
from .enum_mapper import EsphomeEnumMapper
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_InfoT = TypeVar("_InfoT", bound=EntityInfo)
|
||||
_EntityT = TypeVar("_EntityT", bound="EsphomeEntity[Any,Any]")
|
||||
_StateT = TypeVar("_StateT", bound=EntityState)
|
||||
@@ -56,74 +53,21 @@ def async_static_info_updated(
|
||||
) -> None:
|
||||
"""Update entities of this platform when entities are listed."""
|
||||
current_infos = entry_data.info[info_type]
|
||||
device_info = entry_data.device_info
|
||||
if TYPE_CHECKING:
|
||||
assert device_info is not None
|
||||
new_infos: dict[int, EntityInfo] = {}
|
||||
add_entities: list[_EntityT] = []
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
for info in infos:
|
||||
if not current_infos.pop(info.key, None):
|
||||
# Create new entity
|
||||
entity = entity_type(entry_data, platform.domain, info, state_type)
|
||||
add_entities.append(entity)
|
||||
new_infos[info.key] = info
|
||||
|
||||
# Create new entity if it doesn't exist
|
||||
if not (old_info := current_infos.pop(info.key, None)):
|
||||
entity = entity_type(entry_data, platform.domain, info, state_type)
|
||||
add_entities.append(entity)
|
||||
continue
|
||||
|
||||
# Entity exists - check if device_id has changed
|
||||
if old_info.device_id == info.device_id:
|
||||
continue
|
||||
|
||||
# Entity has switched devices, need to migrate unique_id
|
||||
old_unique_id = build_device_unique_id(device_info.mac_address, old_info)
|
||||
entity_id = ent_reg.async_get_entity_id(platform.domain, DOMAIN, old_unique_id)
|
||||
|
||||
# If entity not found in registry, re-add it
|
||||
# This happens when the device_id changed and the old device was deleted
|
||||
if entity_id is None:
|
||||
_LOGGER.info(
|
||||
"Entity with old unique_id %s not found in registry after device_id "
|
||||
"changed from %s to %s, re-adding entity",
|
||||
old_unique_id,
|
||||
old_info.device_id,
|
||||
info.device_id,
|
||||
)
|
||||
entity = entity_type(entry_data, platform.domain, info, state_type)
|
||||
add_entities.append(entity)
|
||||
continue
|
||||
|
||||
updates: dict[str, Any] = {}
|
||||
new_unique_id = build_device_unique_id(device_info.mac_address, info)
|
||||
|
||||
# Update unique_id if it changed
|
||||
if old_unique_id != new_unique_id:
|
||||
updates["new_unique_id"] = new_unique_id
|
||||
|
||||
# Update device assignment
|
||||
if info.device_id:
|
||||
# Entity now belongs to a sub device
|
||||
new_device = dev_reg.async_get_device(
|
||||
identifiers={(DOMAIN, f"{device_info.mac_address}_{info.device_id}")}
|
||||
)
|
||||
else:
|
||||
# Entity now belongs to the main device
|
||||
new_device = dev_reg.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||
)
|
||||
|
||||
if new_device:
|
||||
updates["device_id"] = new_device.id
|
||||
|
||||
# Apply all updates at once
|
||||
if updates:
|
||||
ent_reg.async_update_entity(entity_id, **updates)
|
||||
|
||||
# Anything still in current_infos is now gone
|
||||
if current_infos:
|
||||
device_info = entry_data.device_info
|
||||
if TYPE_CHECKING:
|
||||
assert device_info is not None
|
||||
entry_data.async_remove_entities(
|
||||
hass, current_infos.values(), device_info.mac_address
|
||||
)
|
||||
@@ -300,28 +244,11 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
self._key = entity_info.key
|
||||
self._state_type = state_type
|
||||
self._on_static_info_update(entity_info)
|
||||
|
||||
device_name = device_info.name
|
||||
# Determine the device connection based on whether this entity belongs to a sub device
|
||||
if entity_info.device_id:
|
||||
# Entity belongs to a sub device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, f"{device_info.mac_address}_{entity_info.device_id}")
|
||||
}
|
||||
)
|
||||
# Use the pre-computed device_id_to_name mapping for O(1) lookup
|
||||
device_name = entry_data.device_id_to_name.get(
|
||||
entity_info.device_id, device_info.name
|
||||
)
|
||||
else:
|
||||
# Entity belongs to the main device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||
)
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||
)
|
||||
if entity_info.name:
|
||||
self.entity_id = f"{domain}.{device_name}_{entity_info.name}"
|
||||
self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}"
|
||||
else:
|
||||
# https://github.com/home-assistant/core/issues/132532
|
||||
# If name is not set, ESPHome will use the sanitized friendly name
|
||||
@@ -329,7 +256,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
# as the entity_id before it is sanitized since the sanitizer
|
||||
# is not utf-8 aware. In this case, its always going to be
|
||||
# an empty string so we drop the object_id.
|
||||
self.entity_id = f"{domain}.{device_name}"
|
||||
self.entity_id = f"{domain}.{device_info.name}"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
@@ -363,9 +290,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
static_info = cast(_InfoT, static_info)
|
||||
assert device_info
|
||||
self._static_info = static_info
|
||||
self._attr_unique_id = build_device_unique_id(
|
||||
device_info.mac_address, static_info
|
||||
)
|
||||
self._attr_unique_id = build_unique_id(device_info.mac_address, static_info)
|
||||
self._attr_entity_registry_enabled_default = not static_info.disabled_by_default
|
||||
# https://github.com/home-assistant/core/issues/132532
|
||||
# If the name is "", we need to set it to None since otherwise
|
||||
|
||||
@@ -95,22 +95,6 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = {
|
||||
}
|
||||
|
||||
|
||||
def build_device_unique_id(mac: str, entity_info: EntityInfo) -> str:
|
||||
"""Build unique ID for entity, appending @device_id if it belongs to a sub-device.
|
||||
|
||||
This wrapper around build_unique_id ensures that entities belonging to sub-devices
|
||||
have their device_id appended to the unique_id to handle proper migration when
|
||||
entities move between devices.
|
||||
"""
|
||||
base_unique_id = build_unique_id(mac, entity_info)
|
||||
|
||||
# If entity belongs to a sub-device, append @device_id
|
||||
if entity_info.device_id:
|
||||
return f"{base_unique_id}@{entity_info.device_id}"
|
||||
|
||||
return base_unique_id
|
||||
|
||||
|
||||
class StoreData(TypedDict, total=False):
|
||||
"""ESPHome storage data."""
|
||||
|
||||
@@ -176,7 +160,6 @@ class RuntimeEntryData:
|
||||
assist_satellite_set_wake_word_callbacks: list[Callable[[str], None]] = field(
|
||||
default_factory=list
|
||||
)
|
||||
device_id_to_name: dict[int, str] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -239,9 +222,7 @@ class RuntimeEntryData:
|
||||
ent_reg = er.async_get(hass)
|
||||
for info in static_infos:
|
||||
if entry := ent_reg.async_get_entity_id(
|
||||
INFO_TYPE_TO_PLATFORM[type(info)],
|
||||
DOMAIN,
|
||||
build_device_unique_id(mac, info),
|
||||
INFO_TYPE_TO_PLATFORM[type(info)], DOMAIN, build_unique_id(mac, info)
|
||||
):
|
||||
ent_reg.async_remove(entry)
|
||||
|
||||
@@ -297,8 +278,7 @@ class RuntimeEntryData:
|
||||
if (
|
||||
(old_unique_id := info.unique_id)
|
||||
and (old_entry := registry_get_entity(platform, DOMAIN, old_unique_id))
|
||||
and (new_unique_id := build_device_unique_id(mac, info))
|
||||
!= old_unique_id
|
||||
and (new_unique_id := build_unique_id(mac, info)) != old_unique_id
|
||||
and not registry_get_entity(platform, DOMAIN, new_unique_id)
|
||||
):
|
||||
ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id)
|
||||
|
||||
@@ -527,11 +527,6 @@ class ESPHomeManager:
|
||||
device_info.name,
|
||||
device_mac,
|
||||
)
|
||||
# Build device_id_to_name mapping for efficient lookup
|
||||
entry_data.device_id_to_name = {
|
||||
sub_device.device_id: sub_device.name or device_info.name
|
||||
for sub_device in device_info.devices
|
||||
}
|
||||
self.device_id = _async_setup_device_registry(hass, entry, entry_data)
|
||||
|
||||
entry_data.async_update_device_state()
|
||||
@@ -756,28 +751,6 @@ def _async_setup_device_registry(
|
||||
device_info = entry_data.device_info
|
||||
if TYPE_CHECKING:
|
||||
assert device_info is not None
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
# Build sets of valid device identifiers and connections
|
||||
valid_connections = {
|
||||
(dr.CONNECTION_NETWORK_MAC, format_mac(device_info.mac_address))
|
||||
}
|
||||
valid_identifiers = {
|
||||
(DOMAIN, f"{device_info.mac_address}_{sub_device.device_id}")
|
||||
for sub_device in device_info.devices
|
||||
}
|
||||
|
||||
# Remove devices that no longer exist
|
||||
for device in dr.async_entries_for_config_entry(device_registry, entry.entry_id):
|
||||
# Skip devices we want to keep
|
||||
if (
|
||||
device.connections & valid_connections
|
||||
or device.identifiers & valid_identifiers
|
||||
):
|
||||
continue
|
||||
# Remove everything else
|
||||
device_registry.async_remove_device(device.id)
|
||||
|
||||
sw_version = device_info.esphome_version
|
||||
if device_info.compilation_time:
|
||||
sw_version += f" ({device_info.compilation_time})"
|
||||
@@ -806,14 +779,11 @@ def _async_setup_device_registry(
|
||||
f"{device_info.project_version} (ESPHome {device_info.esphome_version})"
|
||||
)
|
||||
|
||||
suggested_area: str | None = None
|
||||
if device_info.area and device_info.area.name:
|
||||
# Prefer device_info.area over suggested_area when area name is not empty
|
||||
suggested_area = device_info.area.name
|
||||
elif device_info.suggested_area:
|
||||
suggested_area = None
|
||||
if device_info.suggested_area:
|
||||
suggested_area = device_info.suggested_area
|
||||
|
||||
# Create/update main device
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
configuration_url=configuration_url,
|
||||
@@ -824,36 +794,6 @@ def _async_setup_device_registry(
|
||||
sw_version=sw_version,
|
||||
suggested_area=suggested_area,
|
||||
)
|
||||
|
||||
# Handle sub devices
|
||||
# Find available areas from device_info
|
||||
areas_by_id = {area.area_id: area for area in device_info.areas}
|
||||
# Add the main device's area if it exists
|
||||
if device_info.area:
|
||||
areas_by_id[device_info.area.area_id] = device_info.area
|
||||
# Create/update sub devices that should exist
|
||||
for sub_device in device_info.devices:
|
||||
# Determine the area for this sub device
|
||||
sub_device_suggested_area: str | None = None
|
||||
if sub_device.area_id is not None and sub_device.area_id in areas_by_id:
|
||||
sub_device_suggested_area = areas_by_id[sub_device.area_id].name
|
||||
|
||||
sub_device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, f"{device_info.mac_address}_{sub_device.device_id}")},
|
||||
name=sub_device.name or device_entry.name,
|
||||
manufacturer=manufacturer,
|
||||
model=model,
|
||||
sw_version=sw_version,
|
||||
suggested_area=sub_device_suggested_area,
|
||||
)
|
||||
|
||||
# Update the sub device to set via_device_id
|
||||
device_registry.async_update_device(
|
||||
sub_device_entry.id,
|
||||
via_device_id=device_entry.id,
|
||||
)
|
||||
|
||||
return device_entry.id
|
||||
|
||||
|
||||
|
||||
@@ -2,17 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from pyezvizapi.constants import (
|
||||
BatteryCameraWorkMode,
|
||||
DeviceCatagories,
|
||||
DeviceSwitchType,
|
||||
SoundMode,
|
||||
SupportExt,
|
||||
)
|
||||
from pyezvizapi.constants import DeviceSwitchType, SoundMode
|
||||
from pyezvizapi.exceptions import HTTPError, PyEzvizError
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
@@ -32,83 +24,17 @@ class EzvizSelectEntityDescription(SelectEntityDescription):
|
||||
"""Describe a EZVIZ Select entity."""
|
||||
|
||||
supported_switch: int
|
||||
current_option: Callable[[EzvizSelect], str | None]
|
||||
select_option: Callable[[EzvizSelect, str, str], None]
|
||||
|
||||
|
||||
def alarm_sound_mode_current_option(ezvizSelect: EzvizSelect) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
sound_mode_value = getattr(
|
||||
SoundMode, ezvizSelect.data[ezvizSelect.entity_description.key]
|
||||
).value
|
||||
if sound_mode_value in [0, 1, 2]:
|
||||
return ezvizSelect.options[sound_mode_value]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def alarm_sound_mode_select_option(
|
||||
ezvizSelect: EzvizSelect, serial: str, option: str
|
||||
) -> None:
|
||||
"""Change the selected option."""
|
||||
sound_mode_value = ezvizSelect.options.index(option)
|
||||
ezvizSelect.coordinator.ezviz_client.alarm_sound(serial, sound_mode_value, 1)
|
||||
|
||||
|
||||
ALARM_SOUND_MODE_SELECT_TYPE = EzvizSelectEntityDescription(
|
||||
SELECT_TYPE = EzvizSelectEntityDescription(
|
||||
key="alarm_sound_mod",
|
||||
translation_key="alarm_sound_mode",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
options=["soft", "intensive", "silent"],
|
||||
supported_switch=DeviceSwitchType.ALARM_TONE.value,
|
||||
current_option=alarm_sound_mode_current_option,
|
||||
select_option=alarm_sound_mode_select_option,
|
||||
)
|
||||
|
||||
|
||||
def battery_work_mode_current_option(ezvizSelect: EzvizSelect) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
battery_work_mode = getattr(
|
||||
BatteryCameraWorkMode,
|
||||
ezvizSelect.data[ezvizSelect.entity_description.key],
|
||||
BatteryCameraWorkMode.UNKNOWN,
|
||||
)
|
||||
if battery_work_mode == BatteryCameraWorkMode.UNKNOWN:
|
||||
return None
|
||||
|
||||
return battery_work_mode.name.lower()
|
||||
|
||||
|
||||
def battery_work_mode_select_option(
|
||||
ezvizSelect: EzvizSelect, serial: str, option: str
|
||||
) -> None:
|
||||
"""Change the selected option."""
|
||||
battery_work_mode = getattr(BatteryCameraWorkMode, option.upper())
|
||||
ezvizSelect.coordinator.ezviz_client.set_battery_camera_work_mode(
|
||||
serial, battery_work_mode.value
|
||||
)
|
||||
|
||||
|
||||
BATTERY_WORK_MODE_SELECT_TYPE = EzvizSelectEntityDescription(
|
||||
key="battery_camera_work_mode",
|
||||
translation_key="battery_camera_work_mode",
|
||||
icon="mdi:battery-sync",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
options=[
|
||||
"plugged_in",
|
||||
"high_performance",
|
||||
"power_save",
|
||||
"super_power_save",
|
||||
"custom",
|
||||
],
|
||||
supported_switch=-1,
|
||||
current_option=battery_work_mode_current_option,
|
||||
select_option=battery_work_mode_select_option,
|
||||
)
|
||||
|
||||
SELECT_TYPES = [ALARM_SOUND_MODE_SELECT_TYPE, BATTERY_WORK_MODE_SELECT_TYPE]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: EzvizConfigEntry,
|
||||
@@ -117,26 +43,12 @@ async def async_setup_entry(
|
||||
"""Set up EZVIZ select entities based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities = [
|
||||
EzvizSelect(coordinator, camera, ALARM_SOUND_MODE_SELECT_TYPE)
|
||||
async_add_entities(
|
||||
EzvizSelect(coordinator, camera)
|
||||
for camera in coordinator.data
|
||||
for switch in coordinator.data[camera]["switches"]
|
||||
if switch == ALARM_SOUND_MODE_SELECT_TYPE.supported_switch
|
||||
]
|
||||
|
||||
for camera in coordinator.data:
|
||||
device_category = coordinator.data[camera].get("device_category")
|
||||
supportExt = coordinator.data[camera].get("supportExt")
|
||||
if (
|
||||
device_category == DeviceCatagories.BATTERY_CAMERA_DEVICE_CATEGORY.value
|
||||
and supportExt
|
||||
and str(SupportExt.SupportBatteryManage.value) in supportExt
|
||||
):
|
||||
entities.append(
|
||||
EzvizSelect(coordinator, camera, BATTERY_WORK_MODE_SELECT_TYPE)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
if switch == SELECT_TYPE.supported_switch
|
||||
)
|
||||
|
||||
|
||||
class EzvizSelect(EzvizEntity, SelectEntity):
|
||||
@@ -146,23 +58,31 @@ class EzvizSelect(EzvizEntity, SelectEntity):
|
||||
self,
|
||||
coordinator: EzvizDataUpdateCoordinator,
|
||||
serial: str,
|
||||
description: EzvizSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the select entity."""
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, serial)
|
||||
self._attr_unique_id = f"{serial}_{description.key}"
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{serial}_{SELECT_TYPE.key}"
|
||||
self.entity_description = SELECT_TYPE
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
desc = cast(EzvizSelectEntityDescription, self.entity_description)
|
||||
return desc.current_option(self)
|
||||
sound_mode_value = getattr(
|
||||
SoundMode, self.data[self.entity_description.key]
|
||||
).value
|
||||
if sound_mode_value in [0, 1, 2]:
|
||||
return self.options[sound_mode_value]
|
||||
|
||||
return None
|
||||
|
||||
def select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
desc = cast(EzvizSelectEntityDescription, self.entity_description)
|
||||
sound_mode_value = self.options.index(option)
|
||||
|
||||
try:
|
||||
return desc.select_option(self, self._serial, option)
|
||||
self.coordinator.ezviz_client.alarm_sound(self._serial, sound_mode_value, 1)
|
||||
|
||||
except (HTTPError, PyEzvizError) as err:
|
||||
raise HomeAssistantError(f"Cannot select option for {desc.key}") from err
|
||||
raise HomeAssistantError(
|
||||
f"Cannot set Warning sound level for {self.entity_id}"
|
||||
) from err
|
||||
|
||||
@@ -68,16 +68,6 @@
|
||||
"intensive": "Intensive",
|
||||
"silent": "Silent"
|
||||
}
|
||||
},
|
||||
"battery_camera_work_mode": {
|
||||
"name": "Battery work mode",
|
||||
"state": {
|
||||
"plugged_in": "Plugged in",
|
||||
"high_performance": "High performance",
|
||||
"power_save": "Power save",
|
||||
"super_power_save": "Super power saving",
|
||||
"custom": "Custom"
|
||||
}
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
|
||||
@@ -15,9 +15,8 @@ from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import FritzConfigEntry
|
||||
from .coordinator import ConnectionInfo, FritzConfigEntry
|
||||
from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
|
||||
from .models import ConnectionInfo
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -19,10 +19,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import BUTTON_TYPE_WOL, CONNECTION_TYPE_LAN, MeshRoles
|
||||
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
|
||||
from .coordinator import (
|
||||
FRITZ_DATA_KEY,
|
||||
AvmWrapper,
|
||||
FritzConfigEntry,
|
||||
FritzData,
|
||||
FritzDevice,
|
||||
_is_tracked,
|
||||
)
|
||||
from .entity import FritzDeviceBase
|
||||
from .helpers import _is_tracked
|
||||
from .models import FritzDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from collections.abc import Callable, Mapping, ValuesView
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
@@ -34,6 +34,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
@@ -47,15 +48,6 @@ from .const import (
|
||||
FRITZ_EXCEPTIONS,
|
||||
MeshRoles,
|
||||
)
|
||||
from .helpers import _ha_is_stopping
|
||||
from .models import (
|
||||
ConnectionInfo,
|
||||
Device,
|
||||
FritzDevice,
|
||||
HostAttributes,
|
||||
HostInfo,
|
||||
Interface,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,13 +56,33 @@ FRITZ_DATA_KEY: HassKey[FritzData] = HassKey(DOMAIN)
|
||||
type FritzConfigEntry = ConfigEntry[AvmWrapper]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FritzData:
|
||||
"""Storage class for platform global data."""
|
||||
def _is_tracked(mac: str, current_devices: ValuesView[set[str]]) -> bool:
|
||||
"""Check if device is already tracked."""
|
||||
return any(mac in tracked for tracked in current_devices)
|
||||
|
||||
tracked: dict[str, set[str]] = field(default_factory=dict)
|
||||
profile_switches: dict[str, set[str]] = field(default_factory=dict)
|
||||
wol_buttons: dict[str, set[str]] = field(default_factory=dict)
|
||||
|
||||
def device_filter_out_from_trackers(
|
||||
mac: str,
|
||||
device: FritzDevice,
|
||||
current_devices: ValuesView[set[str]],
|
||||
) -> bool:
|
||||
"""Check if device should be filtered out from trackers."""
|
||||
reason: str | None = None
|
||||
if device.ip_address == "":
|
||||
reason = "Missing IP"
|
||||
elif _is_tracked(mac, current_devices):
|
||||
reason = "Already tracked"
|
||||
|
||||
if reason:
|
||||
_LOGGER.debug(
|
||||
"Skip adding device %s [%s], reason: %s", device.hostname, mac, reason
|
||||
)
|
||||
return bool(reason)
|
||||
|
||||
|
||||
def _ha_is_stopping(activity: str) -> None:
|
||||
"""Inform that HA is stopping."""
|
||||
_LOGGER.warning("Cannot execute %s: HomeAssistant is shutting down", activity)
|
||||
|
||||
|
||||
class ClassSetupMissing(Exception):
|
||||
@@ -81,6 +93,68 @@ class ClassSetupMissing(Exception):
|
||||
super().__init__("Function called before Class setup")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device:
|
||||
"""FRITZ!Box device class."""
|
||||
|
||||
connected: bool
|
||||
connected_to: str
|
||||
connection_type: str
|
||||
ip_address: str
|
||||
name: str
|
||||
ssid: str | None
|
||||
wan_access: bool | None = None
|
||||
|
||||
|
||||
class Interface(TypedDict):
|
||||
"""Interface details."""
|
||||
|
||||
device: str
|
||||
mac: str
|
||||
op_mode: str
|
||||
ssid: str | None
|
||||
type: str
|
||||
|
||||
|
||||
HostAttributes = TypedDict(
|
||||
"HostAttributes",
|
||||
{
|
||||
"Index": int,
|
||||
"IPAddress": str,
|
||||
"MACAddress": str,
|
||||
"Active": bool,
|
||||
"HostName": str,
|
||||
"InterfaceType": str,
|
||||
"X_AVM-DE_Port": int,
|
||||
"X_AVM-DE_Speed": int,
|
||||
"X_AVM-DE_UpdateAvailable": bool,
|
||||
"X_AVM-DE_UpdateSuccessful": str,
|
||||
"X_AVM-DE_InfoURL": str | None,
|
||||
"X_AVM-DE_MACAddressList": str | None,
|
||||
"X_AVM-DE_Model": str | None,
|
||||
"X_AVM-DE_URL": str | None,
|
||||
"X_AVM-DE_Guest": bool,
|
||||
"X_AVM-DE_RequestClient": str,
|
||||
"X_AVM-DE_VPN": bool,
|
||||
"X_AVM-DE_WANAccess": str,
|
||||
"X_AVM-DE_Disallow": bool,
|
||||
"X_AVM-DE_IsMeshable": str,
|
||||
"X_AVM-DE_Priority": str,
|
||||
"X_AVM-DE_FriendlyName": str,
|
||||
"X_AVM-DE_FriendlyNameIsWriteable": str,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class HostInfo(TypedDict):
|
||||
"""FRITZ!Box host info class."""
|
||||
|
||||
mac: str
|
||||
name: str
|
||||
ip: str
|
||||
status: bool
|
||||
|
||||
|
||||
class UpdateCoordinatorDataType(TypedDict):
|
||||
"""Update coordinator data type."""
|
||||
|
||||
@@ -824,3 +898,120 @@ class AvmWrapper(FritzBoxTools):
|
||||
"X_AVM-DE_WakeOnLANByMACAddress",
|
||||
NewMACAddress=mac_address,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FritzData:
|
||||
"""Storage class for platform global data."""
|
||||
|
||||
tracked: dict[str, set[str]] = field(default_factory=dict)
|
||||
profile_switches: dict[str, set[str]] = field(default_factory=dict)
|
||||
wol_buttons: dict[str, set[str]] = field(default_factory=dict)
|
||||
|
||||
|
||||
class FritzDevice:
|
||||
"""Representation of a device connected to the FRITZ!Box."""
|
||||
|
||||
def __init__(self, mac: str, name: str) -> None:
|
||||
"""Initialize device info."""
|
||||
self._connected = False
|
||||
self._connected_to: str | None = None
|
||||
self._connection_type: str | None = None
|
||||
self._ip_address: str | None = None
|
||||
self._last_activity: datetime | None = None
|
||||
self._mac = mac
|
||||
self._name = name
|
||||
self._ssid: str | None = None
|
||||
self._wan_access: bool | None = False
|
||||
|
||||
def update(self, dev_info: Device, consider_home: float) -> None:
|
||||
"""Update device info."""
|
||||
utc_point_in_time = dt_util.utcnow()
|
||||
|
||||
if self._last_activity:
|
||||
consider_home_evaluated = (
|
||||
utc_point_in_time - self._last_activity
|
||||
).total_seconds() < consider_home
|
||||
else:
|
||||
consider_home_evaluated = dev_info.connected
|
||||
|
||||
if not self._name:
|
||||
self._name = dev_info.name or self._mac.replace(":", "_")
|
||||
|
||||
self._connected = dev_info.connected or consider_home_evaluated
|
||||
|
||||
if dev_info.connected:
|
||||
self._last_activity = utc_point_in_time
|
||||
|
||||
self._connected_to = dev_info.connected_to
|
||||
self._connection_type = dev_info.connection_type
|
||||
self._ip_address = dev_info.ip_address
|
||||
self._ssid = dev_info.ssid
|
||||
self._wan_access = dev_info.wan_access
|
||||
|
||||
@property
|
||||
def connected_to(self) -> str | None:
|
||||
"""Return connected status."""
|
||||
return self._connected_to
|
||||
|
||||
@property
|
||||
def connection_type(self) -> str | None:
|
||||
"""Return connected status."""
|
||||
return self._connection_type
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""Return connected status."""
|
||||
return self._connected
|
||||
|
||||
@property
|
||||
def mac_address(self) -> str:
|
||||
"""Get MAC address."""
|
||||
return self._mac
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
"""Get Name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def ip_address(self) -> str | None:
|
||||
"""Get IP address."""
|
||||
return self._ip_address
|
||||
|
||||
@property
|
||||
def last_activity(self) -> datetime | None:
|
||||
"""Return device last activity."""
|
||||
return self._last_activity
|
||||
|
||||
@property
|
||||
def ssid(self) -> str | None:
|
||||
"""Return device connected SSID."""
|
||||
return self._ssid
|
||||
|
||||
@property
|
||||
def wan_access(self) -> bool | None:
|
||||
"""Return device wan access."""
|
||||
return self._wan_access
|
||||
|
||||
|
||||
class SwitchInfo(TypedDict):
|
||||
"""FRITZ!Box switch info class."""
|
||||
|
||||
description: str
|
||||
friendly_name: str
|
||||
icon: str
|
||||
type: str
|
||||
callback_update: Callable
|
||||
callback_switch: Callable
|
||||
init_state: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConnectionInfo:
|
||||
"""Fritz sensor connection information class."""
|
||||
|
||||
connection: str
|
||||
mesh_role: MeshRoles
|
||||
wan_enabled: bool
|
||||
ipv6_active: bool
|
||||
|
||||
@@ -10,10 +10,15 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
|
||||
from .coordinator import (
|
||||
FRITZ_DATA_KEY,
|
||||
AvmWrapper,
|
||||
FritzConfigEntry,
|
||||
FritzData,
|
||||
FritzDevice,
|
||||
device_filter_out_from_trackers,
|
||||
)
|
||||
from .entity import FritzDeviceBase
|
||||
from .helpers import device_filter_out_from_trackers
|
||||
from .models import FritzDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_DEVICE_NAME, DOMAIN
|
||||
from .coordinator import AvmWrapper
|
||||
from .models import FritzDevice
|
||||
from .coordinator import AvmWrapper, FritzDevice
|
||||
|
||||
|
||||
class FritzDeviceBase(CoordinatorEntity[AvmWrapper]):
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
"""Helpers for AVM FRITZ!Box."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import ValuesView
|
||||
import logging
|
||||
|
||||
from .models import FritzDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_tracked(mac: str, current_devices: ValuesView[set[str]]) -> bool:
|
||||
"""Check if device is already tracked."""
|
||||
return any(mac in tracked for tracked in current_devices)
|
||||
|
||||
|
||||
def device_filter_out_from_trackers(
|
||||
mac: str,
|
||||
device: FritzDevice,
|
||||
current_devices: ValuesView[set[str]],
|
||||
) -> bool:
|
||||
"""Check if device should be filtered out from trackers."""
|
||||
reason: str | None = None
|
||||
if device.ip_address == "":
|
||||
reason = "Missing IP"
|
||||
elif _is_tracked(mac, current_devices):
|
||||
reason = "Already tracked"
|
||||
|
||||
if reason:
|
||||
_LOGGER.debug(
|
||||
"Skip adding device %s [%s], reason: %s", device.hostname, mac, reason
|
||||
)
|
||||
return bool(reason)
|
||||
|
||||
|
||||
def _ha_is_stopping(activity: str) -> None:
|
||||
"""Inform that HA is stopping."""
|
||||
_LOGGER.warning("Cannot execute %s: HomeAssistant is shutting down", activity)
|
||||
@@ -1,182 +0,0 @@
|
||||
"""Models for AVM FRITZ!Box."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import TypedDict
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import MeshRoles
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device:
|
||||
"""FRITZ!Box device class."""
|
||||
|
||||
connected: bool
|
||||
connected_to: str
|
||||
connection_type: str
|
||||
ip_address: str
|
||||
name: str
|
||||
ssid: str | None
|
||||
wan_access: bool | None = None
|
||||
|
||||
|
||||
class Interface(TypedDict):
|
||||
"""Interface details."""
|
||||
|
||||
device: str
|
||||
mac: str
|
||||
op_mode: str
|
||||
ssid: str | None
|
||||
type: str
|
||||
|
||||
|
||||
HostAttributes = TypedDict(
|
||||
"HostAttributes",
|
||||
{
|
||||
"Index": int,
|
||||
"IPAddress": str,
|
||||
"MACAddress": str,
|
||||
"Active": bool,
|
||||
"HostName": str,
|
||||
"InterfaceType": str,
|
||||
"X_AVM-DE_Port": int,
|
||||
"X_AVM-DE_Speed": int,
|
||||
"X_AVM-DE_UpdateAvailable": bool,
|
||||
"X_AVM-DE_UpdateSuccessful": str,
|
||||
"X_AVM-DE_InfoURL": str | None,
|
||||
"X_AVM-DE_MACAddressList": str | None,
|
||||
"X_AVM-DE_Model": str | None,
|
||||
"X_AVM-DE_URL": str | None,
|
||||
"X_AVM-DE_Guest": bool,
|
||||
"X_AVM-DE_RequestClient": str,
|
||||
"X_AVM-DE_VPN": bool,
|
||||
"X_AVM-DE_WANAccess": str,
|
||||
"X_AVM-DE_Disallow": bool,
|
||||
"X_AVM-DE_IsMeshable": str,
|
||||
"X_AVM-DE_Priority": str,
|
||||
"X_AVM-DE_FriendlyName": str,
|
||||
"X_AVM-DE_FriendlyNameIsWriteable": str,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class HostInfo(TypedDict):
|
||||
"""FRITZ!Box host info class."""
|
||||
|
||||
mac: str
|
||||
name: str
|
||||
ip: str
|
||||
status: bool
|
||||
|
||||
|
||||
class FritzDevice:
|
||||
"""Representation of a device connected to the FRITZ!Box."""
|
||||
|
||||
def __init__(self, mac: str, name: str) -> None:
|
||||
"""Initialize device info."""
|
||||
self._connected = False
|
||||
self._connected_to: str | None = None
|
||||
self._connection_type: str | None = None
|
||||
self._ip_address: str | None = None
|
||||
self._last_activity: datetime | None = None
|
||||
self._mac = mac
|
||||
self._name = name
|
||||
self._ssid: str | None = None
|
||||
self._wan_access: bool | None = False
|
||||
|
||||
def update(self, dev_info: Device, consider_home: float) -> None:
|
||||
"""Update device info."""
|
||||
utc_point_in_time = dt_util.utcnow()
|
||||
|
||||
if self._last_activity:
|
||||
consider_home_evaluated = (
|
||||
utc_point_in_time - self._last_activity
|
||||
).total_seconds() < consider_home
|
||||
else:
|
||||
consider_home_evaluated = dev_info.connected
|
||||
|
||||
if not self._name:
|
||||
self._name = dev_info.name or self._mac.replace(":", "_")
|
||||
|
||||
self._connected = dev_info.connected or consider_home_evaluated
|
||||
|
||||
if dev_info.connected:
|
||||
self._last_activity = utc_point_in_time
|
||||
|
||||
self._connected_to = dev_info.connected_to
|
||||
self._connection_type = dev_info.connection_type
|
||||
self._ip_address = dev_info.ip_address
|
||||
self._ssid = dev_info.ssid
|
||||
self._wan_access = dev_info.wan_access
|
||||
|
||||
@property
|
||||
def connected_to(self) -> str | None:
|
||||
"""Return connected status."""
|
||||
return self._connected_to
|
||||
|
||||
@property
|
||||
def connection_type(self) -> str | None:
|
||||
"""Return connected status."""
|
||||
return self._connection_type
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""Return connected status."""
|
||||
return self._connected
|
||||
|
||||
@property
|
||||
def mac_address(self) -> str:
|
||||
"""Get MAC address."""
|
||||
return self._mac
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
"""Get Name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def ip_address(self) -> str | None:
|
||||
"""Get IP address."""
|
||||
return self._ip_address
|
||||
|
||||
@property
|
||||
def last_activity(self) -> datetime | None:
|
||||
"""Return device last activity."""
|
||||
return self._last_activity
|
||||
|
||||
@property
|
||||
def ssid(self) -> str | None:
|
||||
"""Return device connected SSID."""
|
||||
return self._ssid
|
||||
|
||||
@property
|
||||
def wan_access(self) -> bool | None:
|
||||
"""Return device wan access."""
|
||||
return self._wan_access
|
||||
|
||||
|
||||
class SwitchInfo(TypedDict):
|
||||
"""FRITZ!Box switch info class."""
|
||||
|
||||
description: str
|
||||
friendly_name: str
|
||||
icon: str
|
||||
type: str
|
||||
callback_update: Callable
|
||||
callback_switch: Callable
|
||||
init_state: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConnectionInfo:
|
||||
"""Fritz sensor connection information class."""
|
||||
|
||||
connection: str
|
||||
mesh_role: MeshRoles
|
||||
wan_enabled: bool
|
||||
ipv6_active: bool
|
||||
@@ -27,9 +27,8 @@ from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import DSL_CONNECTION, UPTIME_DEVIATION
|
||||
from .coordinator import FritzConfigEntry
|
||||
from .coordinator import ConnectionInfo, FritzConfigEntry
|
||||
from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
|
||||
from .models import ConnectionInfo
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -25,10 +25,16 @@ from .const import (
|
||||
WIFI_STANDARD,
|
||||
MeshRoles,
|
||||
)
|
||||
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
|
||||
from .coordinator import (
|
||||
FRITZ_DATA_KEY,
|
||||
AvmWrapper,
|
||||
FritzConfigEntry,
|
||||
FritzData,
|
||||
FritzDevice,
|
||||
SwitchInfo,
|
||||
device_filter_out_from_trackers,
|
||||
)
|
||||
from .entity import FritzBoxBaseEntity, FritzDeviceBase
|
||||
from .helpers import device_filter_out_from_trackers
|
||||
from .models import FritzDevice, SwitchInfo
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250625.0"]
|
||||
"requirements": ["home-assistant-frontend==20250531.4"]
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
CONF_PROMPT,
|
||||
DOMAIN,
|
||||
FILE_POLLING_INTERVAL_SECONDS,
|
||||
@@ -189,7 +190,7 @@ async def async_setup_entry(
|
||||
|
||||
client = await hass.async_add_executor_job(_init_client)
|
||||
await client.aio.models.get(
|
||||
model=RECOMMENDED_CHAT_MODEL,
|
||||
model=entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
)
|
||||
except (APIError, Timeout) as err:
|
||||
|
||||
@@ -337,7 +337,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
tools = tools or []
|
||||
tools.append(Tool(google_search=GoogleSearch()))
|
||||
|
||||
model_name = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
||||
model_name = self.entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
||||
# Avoid INVALID_ARGUMENT Developer instruction is not enabled for <model>
|
||||
supports_system_instruction = (
|
||||
"gemma" not in model_name
|
||||
@@ -389,13 +389,47 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
|
||||
if tool_results:
|
||||
messages.append(_create_google_tool_response_content(tool_results))
|
||||
generateContentConfig = self.create_generate_content_config()
|
||||
generateContentConfig.tools = tools or None
|
||||
generateContentConfig.system_instruction = (
|
||||
prompt if supports_system_instruction else None
|
||||
)
|
||||
generateContentConfig.automatic_function_calling = (
|
||||
AutomaticFunctionCallingConfig(disable=True, maximum_remote_calls=None)
|
||||
generateContentConfig = GenerateContentConfig(
|
||||
temperature=self.entry.options.get(
|
||||
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
|
||||
),
|
||||
top_k=self.entry.options.get(CONF_TOP_K, RECOMMENDED_TOP_K),
|
||||
top_p=self.entry.options.get(CONF_TOP_P, RECOMMENDED_TOP_P),
|
||||
max_output_tokens=self.entry.options.get(
|
||||
CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS
|
||||
),
|
||||
safety_settings=[
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
||||
threshold=self.entry.options.get(
|
||||
CONF_HATE_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
),
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_HARASSMENT,
|
||||
threshold=self.entry.options.get(
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD,
|
||||
RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
),
|
||||
),
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold=self.entry.options.get(
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
),
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||||
threshold=self.entry.options.get(
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
),
|
||||
],
|
||||
tools=tools or None,
|
||||
system_instruction=prompt if supports_system_instruction else None,
|
||||
automatic_function_calling=AutomaticFunctionCallingConfig(
|
||||
disable=True, maximum_remote_calls=None
|
||||
),
|
||||
)
|
||||
|
||||
if not supports_system_instruction:
|
||||
@@ -438,40 +472,3 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
|
||||
if not chat_log.unresponded_tool_results:
|
||||
break
|
||||
|
||||
def create_generate_content_config(self) -> GenerateContentConfig:
|
||||
"""Create the GenerateContentConfig for the LLM."""
|
||||
options = self.subentry.data
|
||||
return GenerateContentConfig(
|
||||
temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
|
||||
top_k=options.get(CONF_TOP_K, RECOMMENDED_TOP_K),
|
||||
top_p=options.get(CONF_TOP_P, RECOMMENDED_TOP_P),
|
||||
max_output_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
|
||||
safety_settings=[
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
||||
threshold=options.get(
|
||||
CONF_HATE_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
),
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_HARASSMENT,
|
||||
threshold=options.get(
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD,
|
||||
RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
),
|
||||
),
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold=options.get(
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
),
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||||
threshold=options.get(
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -92,6 +92,7 @@ from .const import (
|
||||
ATTR_LOCATION,
|
||||
ATTR_PASSWORD,
|
||||
ATTR_SLUG,
|
||||
COORDINATOR,
|
||||
DATA_COMPONENT,
|
||||
DATA_CONFIG_STORE,
|
||||
DATA_CORE_INFO,
|
||||
@@ -106,6 +107,7 @@ from .const import (
|
||||
HASSIO_UPDATE_INTERVAL,
|
||||
)
|
||||
from .coordinator import (
|
||||
HassioAddOnDataUpdateCoordinator,
|
||||
HassioDataUpdateCoordinator,
|
||||
get_addons_info,
|
||||
get_addons_stats, # noqa: F401
|
||||
@@ -555,9 +557,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
coordinator = HassioDataUpdateCoordinator(hass, entry, dev_reg)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[ADDONS_COORDINATOR] = coordinator
|
||||
hass.data[COORDINATOR] = coordinator
|
||||
|
||||
addon_coordinator = HassioAddOnDataUpdateCoordinator(hass, entry, dev_reg)
|
||||
await addon_coordinator.async_config_entry_first_refresh()
|
||||
hass.data[ADDONS_COORDINATOR] = addon_coordinator
|
||||
|
||||
def deprecated_setup_issue() -> None:
|
||||
os_info = get_os_info(hass)
|
||||
|
||||
@@ -41,15 +41,15 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Binary sensor set up for Hass.io config entry."""
|
||||
coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
addons_coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
|
||||
async_add_entities(
|
||||
HassioAddonBinarySensor(
|
||||
addon=addon,
|
||||
coordinator=coordinator,
|
||||
coordinator=addons_coordinator,
|
||||
entity_description=entity_description,
|
||||
)
|
||||
for addon in coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for entity_description in ADDON_ENTITY_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ EVENT_ISSUE_REMOVED = "issue_removed"
|
||||
|
||||
UPDATE_KEY_SUPERVISOR = "supervisor"
|
||||
|
||||
COORDINATOR = "hassio_coordinator"
|
||||
ADDONS_COORDINATOR = "hassio_addons_coordinator"
|
||||
|
||||
|
||||
@@ -85,9 +86,11 @@ DATA_OS_INFO = "hassio_os_info"
|
||||
DATA_NETWORK_INFO = "hassio_network_info"
|
||||
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
||||
DATA_SUPERVISOR_STATS = "hassio_supervisor_stats"
|
||||
DATA_ADDONS = "hassio_addons"
|
||||
DATA_ADDONS_INFO = "hassio_addons_info"
|
||||
DATA_ADDONS_STATS = "hassio_addons_stats"
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
HASSIO_ADDON_UPDATE_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
ATTR_AUTO_UPDATE = "auto_update"
|
||||
ATTR_VERSION = "version"
|
||||
|
||||
@@ -30,6 +30,7 @@ from .const import (
|
||||
CONTAINER_INFO,
|
||||
CONTAINER_STATS,
|
||||
CORE_CONTAINER,
|
||||
DATA_ADDONS,
|
||||
DATA_ADDONS_INFO,
|
||||
DATA_ADDONS_STATS,
|
||||
DATA_COMPONENT,
|
||||
@@ -49,6 +50,7 @@ from .const import (
|
||||
DATA_SUPERVISOR_INFO,
|
||||
DATA_SUPERVISOR_STATS,
|
||||
DOMAIN,
|
||||
HASSIO_ADDON_UPDATE_INTERVAL,
|
||||
HASSIO_UPDATE_INTERVAL,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
SUPERVISOR_CONTAINER,
|
||||
@@ -112,6 +114,16 @@ def get_network_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
return hass.data.get(DATA_NETWORK_INFO)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_addons(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
"""Return Addons info.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return hass.data.get(DATA_ADDONS)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_addons_info(hass: HomeAssistant) -> dict[str, dict[str, Any]] | None:
|
||||
@@ -279,8 +291,8 @@ def async_remove_addons_from_dev_reg(
|
||||
dev_reg.async_remove_device(dev.id)
|
||||
|
||||
|
||||
class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to retrieve Hass.io status."""
|
||||
class HassioAddOnDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to retrieve Hass.io Add-on status."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
@@ -293,7 +305,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=HASSIO_UPDATE_INTERVAL,
|
||||
update_interval=HASSIO_ADDON_UPDATE_INTERVAL,
|
||||
# We don't want an immediate refresh since we want to avoid
|
||||
# fetching the container stats right away and avoid hammering
|
||||
# the Supervisor API on startup
|
||||
@@ -305,7 +317,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
self.data = {}
|
||||
self.entry_id = config_entry.entry_id
|
||||
self.dev_reg = dev_reg
|
||||
self.is_hass_os = (get_info(self.hass) or {}).get("hassos") is not None
|
||||
self._container_updates: defaultdict[str, dict[str, set[str]]] = defaultdict(
|
||||
lambda: defaultdict(set)
|
||||
)
|
||||
@@ -321,7 +332,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
raise UpdateFailed(f"Error on Supervisor API: {err}") from err
|
||||
|
||||
new_data: dict[str, Any] = {}
|
||||
supervisor_info = get_supervisor_info(self.hass) or {}
|
||||
addons = get_addons(self.hass) or {}
|
||||
addons_info = get_addons_info(self.hass) or {}
|
||||
addons_stats = get_addons_stats(self.hass)
|
||||
store_data = get_store(self.hass)
|
||||
@@ -345,37 +356,14 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "")
|
||||
),
|
||||
}
|
||||
for addon in supervisor_info.get("addons", [])
|
||||
for addon in addons.get("addons", [])
|
||||
}
|
||||
if self.is_hass_os:
|
||||
new_data[DATA_KEY_OS] = get_os_info(self.hass)
|
||||
|
||||
new_data[DATA_KEY_CORE] = {
|
||||
**(get_core_info(self.hass) or {}),
|
||||
**get_core_stats(self.hass),
|
||||
}
|
||||
new_data[DATA_KEY_SUPERVISOR] = {
|
||||
**supervisor_info,
|
||||
**get_supervisor_stats(self.hass),
|
||||
}
|
||||
new_data[DATA_KEY_HOST] = get_host_info(self.hass) or {}
|
||||
|
||||
# If this is the initial refresh, register all addons and return the dict
|
||||
if is_first_update:
|
||||
async_register_addons_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values()
|
||||
)
|
||||
async_register_core_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_CORE]
|
||||
)
|
||||
async_register_supervisor_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_SUPERVISOR]
|
||||
)
|
||||
async_register_host_in_dev_reg(self.entry_id, self.dev_reg)
|
||||
if self.is_hass_os:
|
||||
async_register_os_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_OS]
|
||||
)
|
||||
|
||||
# Remove add-ons that are no longer installed from device registry
|
||||
supervisor_addon_devices = {
|
||||
@@ -388,12 +376,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]):
|
||||
async_remove_addons_from_dev_reg(self.dev_reg, stale_addons)
|
||||
|
||||
if not self.is_hass_os and (
|
||||
dev := self.dev_reg.async_get_device(identifiers={(DOMAIN, "OS")})
|
||||
):
|
||||
# Remove the OS device if it exists and the installation is not hassos
|
||||
self.dev_reg.async_remove_device(dev.id)
|
||||
|
||||
# If there are new add-ons, we should reload the config entry so we can
|
||||
# create new devices and entities. We can return an empty dict because
|
||||
# coordinator will be recreated.
|
||||
@@ -419,23 +401,9 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
container_updates = self._container_updates
|
||||
|
||||
data = self.hass.data
|
||||
hassio = self.hassio
|
||||
updates = {
|
||||
DATA_INFO: hassio.get_info(),
|
||||
DATA_CORE_INFO: hassio.get_core_info(),
|
||||
DATA_SUPERVISOR_INFO: hassio.get_supervisor_info(),
|
||||
DATA_OS_INFO: hassio.get_os_info(),
|
||||
}
|
||||
if CONTAINER_STATS in container_updates[CORE_CONTAINER]:
|
||||
updates[DATA_CORE_STATS] = hassio.get_core_stats()
|
||||
if CONTAINER_STATS in container_updates[SUPERVISOR_CONTAINER]:
|
||||
updates[DATA_SUPERVISOR_STATS] = hassio.get_supervisor_stats()
|
||||
data[DATA_ADDONS] = await self.hassio.get_addons()
|
||||
|
||||
results = await asyncio.gather(*updates.values())
|
||||
for key, result in zip(updates, results, strict=False):
|
||||
data[key] = result
|
||||
|
||||
_addon_data = data[DATA_SUPERVISOR_INFO].get("addons", [])
|
||||
_addon_data = data[DATA_ADDONS].get("addons", [])
|
||||
all_addons: list[str] = []
|
||||
started_addons: list[str] = []
|
||||
for addon in _addon_data:
|
||||
@@ -531,14 +499,159 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
) -> None:
|
||||
"""Refresh data."""
|
||||
if not scheduled and not raise_on_auth_failed:
|
||||
# Force refreshing updates for non-scheduled updates
|
||||
# Force reloading add-on updates for non-scheduled
|
||||
# updates.
|
||||
#
|
||||
# If `raise_on_auth_failed` is set, it means this is
|
||||
# the first refresh and we do not want to delay
|
||||
# startup or cause a timeout so we only refresh the
|
||||
# updates if this is not a scheduled refresh and
|
||||
# we are not doing the first refresh.
|
||||
try:
|
||||
await self.supervisor_client.refresh_updates()
|
||||
await self.supervisor_client.store.reload()
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Error on Supervisor API: %s", err)
|
||||
|
||||
await super()._async_refresh(
|
||||
log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error
|
||||
)
|
||||
|
||||
|
||||
class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to retrieve Hass.io status."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, dev_reg: dr.DeviceRegistry
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=HASSIO_UPDATE_INTERVAL,
|
||||
# We don't want an immediate refresh since we want to avoid
|
||||
# fetching the container stats right away and avoid hammering
|
||||
# the Supervisor API on startup
|
||||
request_refresh_debouncer=Debouncer(
|
||||
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
|
||||
),
|
||||
)
|
||||
self.hassio = hass.data[DATA_COMPONENT]
|
||||
self.data = {}
|
||||
self.entry_id = config_entry.entry_id
|
||||
self.dev_reg = dev_reg
|
||||
self.is_hass_os = (get_info(self.hass) or {}).get("hassos") is not None
|
||||
self._container_updates: defaultdict[str, dict[str, set[str]]] = defaultdict(
|
||||
lambda: defaultdict(set)
|
||||
)
|
||||
self.supervisor_client = get_supervisor_client(hass)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
is_first_update = not self.data
|
||||
|
||||
try:
|
||||
await self.force_data_refresh(is_first_update)
|
||||
except HassioAPIError as err:
|
||||
raise UpdateFailed(f"Error on Supervisor API: {err}") from err
|
||||
|
||||
new_data: dict[str, Any] = {}
|
||||
supervisor_info = get_supervisor_info(self.hass) or {}
|
||||
|
||||
if self.is_hass_os:
|
||||
new_data[DATA_KEY_OS] = get_os_info(self.hass)
|
||||
|
||||
new_data[DATA_KEY_CORE] = {
|
||||
**(get_core_info(self.hass) or {}),
|
||||
**get_core_stats(self.hass),
|
||||
}
|
||||
new_data[DATA_KEY_SUPERVISOR] = {
|
||||
**supervisor_info,
|
||||
**get_supervisor_stats(self.hass),
|
||||
}
|
||||
new_data[DATA_KEY_HOST] = get_host_info(self.hass) or {}
|
||||
|
||||
# If this is the initial refresh, register all main components
|
||||
if is_first_update:
|
||||
async_register_core_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_CORE]
|
||||
)
|
||||
async_register_supervisor_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_SUPERVISOR]
|
||||
)
|
||||
async_register_host_in_dev_reg(self.entry_id, self.dev_reg)
|
||||
if self.is_hass_os:
|
||||
async_register_os_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_OS]
|
||||
)
|
||||
|
||||
if not self.is_hass_os and (
|
||||
dev := self.dev_reg.async_get_device(identifiers={(DOMAIN, "OS")})
|
||||
):
|
||||
# Remove the OS device if it exists and the installation is not hassos
|
||||
self.dev_reg.async_remove_device(dev.id)
|
||||
|
||||
return new_data
|
||||
|
||||
async def force_data_refresh(self, first_update: bool) -> None:
|
||||
"""Force update of the addon info."""
|
||||
container_updates = self._container_updates
|
||||
|
||||
data = self.hass.data
|
||||
hassio = self.hassio
|
||||
updates = {
|
||||
DATA_INFO: hassio.get_info(),
|
||||
DATA_CORE_INFO: hassio.get_core_info(),
|
||||
DATA_SUPERVISOR_INFO: hassio.get_supervisor_info(),
|
||||
DATA_OS_INFO: hassio.get_os_info(),
|
||||
}
|
||||
if CONTAINER_STATS in container_updates[CORE_CONTAINER]:
|
||||
updates[DATA_CORE_STATS] = hassio.get_core_stats()
|
||||
if CONTAINER_STATS in container_updates[SUPERVISOR_CONTAINER]:
|
||||
updates[DATA_SUPERVISOR_STATS] = hassio.get_supervisor_stats()
|
||||
|
||||
results = await asyncio.gather(*updates.values())
|
||||
for key, result in zip(updates, results, strict=False):
|
||||
data[key] = result
|
||||
|
||||
@callback
|
||||
def async_enable_container_updates(
|
||||
self, slug: str, entity_id: str, types: set[str]
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Enable updates for an add-on."""
|
||||
enabled_updates = self._container_updates[slug]
|
||||
for key in types:
|
||||
enabled_updates[key].add(entity_id)
|
||||
|
||||
@callback
|
||||
def _remove() -> None:
|
||||
for key in types:
|
||||
enabled_updates[key].remove(entity_id)
|
||||
|
||||
return _remove
|
||||
|
||||
async def _async_refresh(
|
||||
self,
|
||||
log_failures: bool = True,
|
||||
raise_on_auth_failed: bool = False,
|
||||
scheduled: bool = False,
|
||||
raise_on_entry_error: bool = False,
|
||||
) -> None:
|
||||
"""Refresh data."""
|
||||
if not scheduled and not raise_on_auth_failed:
|
||||
# Force reloading updates of main components for
|
||||
# non-scheduled updates.
|
||||
#
|
||||
# If `raise_on_auth_failed` is set, it means this is
|
||||
# the first refresh and we do not want to delay
|
||||
# startup or cause a timeout so we only refresh the
|
||||
# updates if this is not a scheduled refresh and
|
||||
# we are not doing the first refresh.
|
||||
try:
|
||||
await self.supervisor_client.reload_updates()
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Error on Supervisor API: %s", err)
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .const import ADDONS_COORDINATOR
|
||||
from .coordinator import HassioDataUpdateCoordinator
|
||||
from .const import ADDONS_COORDINATOR, COORDINATOR
|
||||
from .coordinator import HassioAddOnDataUpdateCoordinator, HassioDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
@@ -19,7 +19,8 @@ async def async_get_config_entry_diagnostics(
|
||||
config_entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: HassioDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
|
||||
coordinator: HassioDataUpdateCoordinator = hass.data[COORDINATOR]
|
||||
addons_coordinator: HassioAddOnDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
@@ -50,5 +51,6 @@ async def async_get_config_entry_diagnostics(
|
||||
|
||||
return {
|
||||
"coordinator_data": coordinator.data,
|
||||
"addons_coordinator_data": addons_coordinator.data,
|
||||
"devices": devices,
|
||||
}
|
||||
|
||||
@@ -21,17 +21,17 @@ from .const import (
|
||||
KEY_TO_UPDATE_TYPES,
|
||||
SUPERVISOR_CONTAINER,
|
||||
)
|
||||
from .coordinator import HassioDataUpdateCoordinator
|
||||
from .coordinator import HassioAddOnDataUpdateCoordinator, HassioDataUpdateCoordinator
|
||||
|
||||
|
||||
class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
|
||||
class HassioAddonEntity(CoordinatorEntity[HassioAddOnDataUpdateCoordinator]):
|
||||
"""Base entity for a Hass.io add-on."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HassioDataUpdateCoordinator,
|
||||
coordinator: HassioAddOnDataUpdateCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
addon: dict[str, Any],
|
||||
) -> None:
|
||||
|
||||
@@ -226,6 +226,14 @@ class HassIO:
|
||||
"""
|
||||
return self.send_command("/ingress/panels", method="get")
|
||||
|
||||
@api_data
|
||||
def get_addons(self) -> Coroutine:
|
||||
"""Return data installed Add-ons.
|
||||
|
||||
This method returns a coroutine.
|
||||
"""
|
||||
return self.send_command("/addons", method="get")
|
||||
|
||||
@_api_bool
|
||||
async def update_hass_api(
|
||||
self, http_config: dict[str, Any], refresh_token: RefreshToken
|
||||
|
||||
@@ -19,6 +19,7 @@ from .const import (
|
||||
ATTR_MEMORY_PERCENT,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
COORDINATOR,
|
||||
DATA_KEY_ADDONS,
|
||||
DATA_KEY_CORE,
|
||||
DATA_KEY_HOST,
|
||||
@@ -114,20 +115,21 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Sensor set up for Hass.io config entry."""
|
||||
coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
addons_coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
|
||||
entities: list[
|
||||
HassioOSSensor | HassioAddonSensor | CoreSensor | SupervisorSensor | HostSensor
|
||||
] = [
|
||||
HassioAddonSensor(
|
||||
addon=addon,
|
||||
coordinator=coordinator,
|
||||
coordinator=addons_coordinator,
|
||||
entity_description=entity_description,
|
||||
)
|
||||
for addon in coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for entity_description in ADDON_ENTITY_DESCRIPTIONS
|
||||
]
|
||||
|
||||
coordinator = hass.data[COORDINATOR]
|
||||
entities.extend(
|
||||
CoreSensor(
|
||||
coordinator=coordinator,
|
||||
|
||||
@@ -24,6 +24,7 @@ from .const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
COORDINATOR,
|
||||
DATA_KEY_ADDONS,
|
||||
DATA_KEY_CORE,
|
||||
DATA_KEY_OS,
|
||||
@@ -49,9 +50,9 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Supervisor update based on a config entry."""
|
||||
coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
coordinator = hass.data[COORDINATOR]
|
||||
|
||||
entities = [
|
||||
entities: list[UpdateEntity] = [
|
||||
SupervisorSupervisorUpdateEntity(
|
||||
coordinator=coordinator,
|
||||
entity_description=ENTITY_DESCRIPTION,
|
||||
@@ -62,15 +63,6 @@ async def async_setup_entry(
|
||||
),
|
||||
]
|
||||
|
||||
entities.extend(
|
||||
SupervisorAddonUpdateEntity(
|
||||
addon=addon,
|
||||
coordinator=coordinator,
|
||||
entity_description=ENTITY_DESCRIPTION,
|
||||
)
|
||||
for addon in coordinator.data[DATA_KEY_ADDONS].values()
|
||||
)
|
||||
|
||||
if coordinator.is_hass_os:
|
||||
entities.append(
|
||||
SupervisorOSUpdateEntity(
|
||||
@@ -79,6 +71,16 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
|
||||
addons_coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
entities.extend(
|
||||
SupervisorAddonUpdateEntity(
|
||||
addon=addon,
|
||||
coordinator=addons_coordinator,
|
||||
entity_description=ENTITY_DESCRIPTION,
|
||||
)
|
||||
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohomeconnect.model import GetSetting, Status
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
@@ -13,30 +11,14 @@ from .const import DOMAIN
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
|
||||
|
||||
def _serialize_item(item: Status | GetSetting) -> dict[str, Any]:
|
||||
"""Serialize a status or setting item to a dictionary."""
|
||||
data = {"value": item.value}
|
||||
if item.unit is not None:
|
||||
data["unit"] = item.unit
|
||||
if item.constraints is not None:
|
||||
data["constraints"] = {
|
||||
k: v for k, v in item.constraints.to_dict().items() if v is not None
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
async def _generate_appliance_diagnostics(
|
||||
appliance: HomeConnectApplianceData,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
**appliance.info.to_dict(),
|
||||
"status": {
|
||||
key.value: _serialize_item(status)
|
||||
for key, status in appliance.status.items()
|
||||
},
|
||||
"status": {key.value: status.value for key, status in appliance.status.items()},
|
||||
"settings": {
|
||||
key.value: _serialize_item(setting)
|
||||
for key, setting in appliance.settings.items()
|
||||
key.value: setting.value for key, setting in appliance.settings.items()
|
||||
},
|
||||
"programs": [program.raw_key for program in appliance.programs],
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"macaddress": "68A40E*"
|
||||
},
|
||||
{
|
||||
"hostname": "(bosch|neff|siemens)-*",
|
||||
"hostname": "(siemens|neff)-*",
|
||||
"macaddress": "38B4D3*"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,8 +7,6 @@ import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ha_silabs_firmware_client import FirmwareUpdateClient
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
AddonError,
|
||||
AddonInfo,
|
||||
@@ -24,17 +22,17 @@ from homeassistant.config_entries import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
|
||||
from . import silabs_multiprotocol_addon
|
||||
from .const import OTBR_DOMAIN, ZHA_DOMAIN
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
OwningAddon,
|
||||
OwningIntegration,
|
||||
async_flash_silabs_firmware,
|
||||
get_otbr_addon_manager,
|
||||
get_zigbee_flasher_addon_manager,
|
||||
guess_firmware_info,
|
||||
guess_hardware_owners,
|
||||
probe_silabs_firmware_info,
|
||||
@@ -63,7 +61,6 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
self.addon_install_task: asyncio.Task | None = None
|
||||
self.addon_start_task: asyncio.Task | None = None
|
||||
self.addon_uninstall_task: asyncio.Task | None = None
|
||||
self.firmware_install_task: asyncio.Task | None = None
|
||||
|
||||
def _get_translation_placeholders(self) -> dict[str, str]:
|
||||
"""Shared translation placeholders."""
|
||||
@@ -80,6 +77,22 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
|
||||
return placeholders
|
||||
|
||||
async def _async_set_addon_config(
|
||||
self, config: dict, addon_manager: AddonManager
|
||||
) -> None:
|
||||
"""Set add-on config."""
|
||||
try:
|
||||
await addon_manager.async_set_addon_options(config)
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
raise AbortFlow(
|
||||
"addon_set_config_failed",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": addon_manager.addon_name,
|
||||
},
|
||||
) from err
|
||||
|
||||
async def _async_get_addon_info(self, addon_manager: AddonManager) -> AddonInfo:
|
||||
"""Return add-on info."""
|
||||
try:
|
||||
@@ -137,54 +150,6 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
)
|
||||
)
|
||||
|
||||
async def _install_firmware_step(
|
||||
self,
|
||||
fw_update_url: str,
|
||||
fw_type: str,
|
||||
firmware_name: str,
|
||||
expected_installed_firmware_type: ApplicationType,
|
||||
step_id: str,
|
||||
next_step_id: str,
|
||||
) -> ConfigFlowResult:
|
||||
assert self._device is not None
|
||||
|
||||
if not self.firmware_install_task:
|
||||
session = async_get_clientsession(self.hass)
|
||||
client = FirmwareUpdateClient(fw_update_url, session)
|
||||
manifest = await client.async_update_data()
|
||||
|
||||
fw_meta = next(
|
||||
fw for fw in manifest.firmwares if fw.filename.startswith(fw_type)
|
||||
)
|
||||
|
||||
fw_data = await client.async_fetch_firmware(fw_meta)
|
||||
self.firmware_install_task = self.hass.async_create_task(
|
||||
async_flash_silabs_firmware(
|
||||
hass=self.hass,
|
||||
device=self._device,
|
||||
fw_data=fw_data,
|
||||
expected_installed_firmware_type=expected_installed_firmware_type,
|
||||
bootloader_reset_type=None,
|
||||
progress_callback=lambda offset, total: self.async_update_progress(
|
||||
offset / total
|
||||
),
|
||||
),
|
||||
f"Flash {firmware_name} firmware",
|
||||
)
|
||||
|
||||
if not self.firmware_install_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id=step_id,
|
||||
progress_action="install_firmware",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"firmware_name": firmware_name,
|
||||
},
|
||||
progress_task=self.firmware_install_task,
|
||||
)
|
||||
|
||||
return self.async_show_progress_done(next_step_id=next_step_id)
|
||||
|
||||
async def async_step_pick_firmware_zigbee(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -195,133 +160,68 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
return await self.async_step_install_zigbee_firmware()
|
||||
# Allow the stick to be used with ZHA without flashing
|
||||
if (
|
||||
self._probed_firmware_info is not None
|
||||
and self._probed_firmware_info.firmware_type == ApplicationType.EZSP
|
||||
):
|
||||
return await self.async_step_confirm_zigbee()
|
||||
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Install Zigbee firmware."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_step_addon_operation_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Abort when add-on installation or start failed."""
|
||||
return self.async_abort(
|
||||
reason=self._failed_addon_reason,
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": self._failed_addon_name,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_confirm_zigbee(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm Zigbee setup."""
|
||||
assert self._device is not None
|
||||
assert self._hardware_name is not None
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="confirm_zigbee",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
if not await self._probe_firmware_info(probe_methods=(ApplicationType.EZSP,)):
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
await self.hass.config_entries.flow.async_init(
|
||||
ZHA_DOMAIN,
|
||||
context={"source": "hardware"},
|
||||
data={
|
||||
"name": self._hardware_name,
|
||||
"port": {
|
||||
"path": self._device,
|
||||
"baudrate": 115200,
|
||||
"flow_control": "hardware",
|
||||
},
|
||||
"radio_type": "ezsp",
|
||||
},
|
||||
)
|
||||
|
||||
return self._async_flow_finished()
|
||||
|
||||
async def _ensure_thread_addon_setup(self) -> ConfigFlowResult | None:
|
||||
"""Ensure the OTBR addon is set up and not running."""
|
||||
|
||||
# We install the OTBR addon no matter what, since it is required to use Thread
|
||||
if not is_hassio(self.hass):
|
||||
return self.async_abort(
|
||||
reason="not_hassio_thread",
|
||||
reason="not_hassio",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
otbr_manager = get_otbr_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(otbr_manager)
|
||||
# Only flash new firmware if we need to
|
||||
fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(fw_flasher_manager)
|
||||
|
||||
if addon_info.state == AddonState.NOT_INSTALLED:
|
||||
return await self.async_step_install_otbr_addon()
|
||||
return await self.async_step_install_zigbee_flasher_addon()
|
||||
|
||||
if addon_info.state == AddonState.RUNNING:
|
||||
# We only fail setup if we have an instance of OTBR running *and* it's
|
||||
# pointing to different hardware
|
||||
if addon_info.options["device"] != self._device:
|
||||
return self.async_abort(
|
||||
reason="otbr_addon_already_running",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": otbr_manager.addon_name,
|
||||
},
|
||||
)
|
||||
if addon_info.state == AddonState.NOT_RUNNING:
|
||||
return await self.async_step_run_zigbee_flasher_addon()
|
||||
|
||||
# Otherwise, stop the addon before continuing to flash firmware
|
||||
await otbr_manager.async_stop_addon()
|
||||
# If the addon is already installed and running, fail
|
||||
return self.async_abort(
|
||||
reason="addon_already_running",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": fw_flasher_manager.addon_name,
|
||||
},
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
async def async_step_pick_firmware_thread(
|
||||
async def async_step_install_zigbee_flasher_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Pick Thread firmware."""
|
||||
if not await self._probe_firmware_info():
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
"""Show progress dialog for installing the Zigbee flasher addon."""
|
||||
return await self._install_addon(
|
||||
get_zigbee_flasher_addon_manager(self.hass),
|
||||
"install_zigbee_flasher_addon",
|
||||
"run_zigbee_flasher_addon",
|
||||
)
|
||||
|
||||
if result := await self._ensure_thread_addon_setup():
|
||||
return result
|
||||
|
||||
return await self.async_step_install_thread_firmware()
|
||||
|
||||
async def async_step_install_thread_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
async def _install_addon(
|
||||
self,
|
||||
addon_manager: silabs_multiprotocol_addon.WaitingAddonManager,
|
||||
step_id: str,
|
||||
next_step_id: str,
|
||||
) -> ConfigFlowResult:
|
||||
"""Install Thread firmware."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_step_install_otbr_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show progress dialog for installing the OTBR addon."""
|
||||
addon_manager = get_otbr_addon_manager(self.hass)
|
||||
"""Show progress dialog for installing an addon."""
|
||||
addon_info = await self._async_get_addon_info(addon_manager)
|
||||
|
||||
_LOGGER.debug("OTBR addon info: %s", addon_info)
|
||||
_LOGGER.debug("Flasher addon state: %s", addon_info)
|
||||
|
||||
if not self.addon_install_task:
|
||||
self.addon_install_task = self.hass.async_create_task(
|
||||
addon_manager.async_install_addon_waiting(),
|
||||
"OTBR addon install",
|
||||
"Addon install",
|
||||
)
|
||||
|
||||
if not self.addon_install_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="install_otbr_addon",
|
||||
step_id=step_id,
|
||||
progress_action="install_addon",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
@@ -340,50 +240,208 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
finally:
|
||||
self.addon_install_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="install_thread_firmware")
|
||||
return self.async_show_progress_done(next_step_id=next_step_id)
|
||||
|
||||
async def async_step_addon_operation_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Abort when add-on installation or start failed."""
|
||||
return self.async_abort(
|
||||
reason=self._failed_addon_reason,
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": self._failed_addon_name,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_run_zigbee_flasher_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Configure the flasher addon to point to the SkyConnect and run it."""
|
||||
fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(fw_flasher_manager)
|
||||
|
||||
assert self._device is not None
|
||||
new_addon_config = {
|
||||
**addon_info.options,
|
||||
"device": self._device,
|
||||
"baudrate": 115200,
|
||||
"bootloader_baudrate": 115200,
|
||||
"flow_control": True,
|
||||
}
|
||||
|
||||
_LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config)
|
||||
await self._async_set_addon_config(new_addon_config, fw_flasher_manager)
|
||||
|
||||
if not self.addon_start_task:
|
||||
|
||||
async def start_and_wait_until_done() -> None:
|
||||
await fw_flasher_manager.async_start_addon_waiting()
|
||||
# Now that the addon is running, wait for it to finish
|
||||
await fw_flasher_manager.async_wait_until_addon_state(
|
||||
AddonState.NOT_RUNNING
|
||||
)
|
||||
|
||||
self.addon_start_task = self.hass.async_create_task(
|
||||
start_and_wait_until_done()
|
||||
)
|
||||
|
||||
if not self.addon_start_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="run_zigbee_flasher_addon",
|
||||
progress_action="run_zigbee_flasher_addon",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": fw_flasher_manager.addon_name,
|
||||
},
|
||||
progress_task=self.addon_start_task,
|
||||
)
|
||||
|
||||
try:
|
||||
await self.addon_start_task
|
||||
except (AddonError, AbortFlow) as err:
|
||||
_LOGGER.error(err)
|
||||
self._failed_addon_name = fw_flasher_manager.addon_name
|
||||
self._failed_addon_reason = "addon_start_failed"
|
||||
return self.async_show_progress_done(next_step_id="addon_operation_failed")
|
||||
finally:
|
||||
self.addon_start_task = None
|
||||
|
||||
return self.async_show_progress_done(
|
||||
next_step_id="uninstall_zigbee_flasher_addon"
|
||||
)
|
||||
|
||||
async def async_step_uninstall_zigbee_flasher_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Uninstall the flasher addon."""
|
||||
fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass)
|
||||
|
||||
if not self.addon_uninstall_task:
|
||||
_LOGGER.debug("Uninstalling flasher addon")
|
||||
self.addon_uninstall_task = self.hass.async_create_task(
|
||||
fw_flasher_manager.async_uninstall_addon_waiting()
|
||||
)
|
||||
|
||||
if not self.addon_uninstall_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="uninstall_zigbee_flasher_addon",
|
||||
progress_action="uninstall_zigbee_flasher_addon",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": fw_flasher_manager.addon_name,
|
||||
},
|
||||
progress_task=self.addon_uninstall_task,
|
||||
)
|
||||
|
||||
try:
|
||||
await self.addon_uninstall_task
|
||||
except (AddonError, AbortFlow) as err:
|
||||
_LOGGER.error(err)
|
||||
# The uninstall failing isn't critical so we can just continue
|
||||
finally:
|
||||
self.addon_uninstall_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="confirm_zigbee")
|
||||
|
||||
async def async_step_confirm_zigbee(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm Zigbee setup."""
|
||||
assert self._device is not None
|
||||
assert self._hardware_name is not None
|
||||
|
||||
if not await self._probe_firmware_info(probe_methods=(ApplicationType.EZSP,)):
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
await self.hass.config_entries.flow.async_init(
|
||||
ZHA_DOMAIN,
|
||||
context={"source": "hardware"},
|
||||
data={
|
||||
"name": self._hardware_name,
|
||||
"port": {
|
||||
"path": self._device,
|
||||
"baudrate": 115200,
|
||||
"flow_control": "hardware",
|
||||
},
|
||||
"radio_type": "ezsp",
|
||||
},
|
||||
)
|
||||
|
||||
return self._async_flow_finished()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm_zigbee",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
async def async_step_pick_firmware_thread(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Pick Thread firmware."""
|
||||
if not await self._probe_firmware_info():
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
# We install the OTBR addon no matter what, since it is required to use Thread
|
||||
if not is_hassio(self.hass):
|
||||
return self.async_abort(
|
||||
reason="not_hassio_thread",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
otbr_manager = get_otbr_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(otbr_manager)
|
||||
|
||||
if addon_info.state == AddonState.NOT_INSTALLED:
|
||||
return await self.async_step_install_otbr_addon()
|
||||
|
||||
if addon_info.state == AddonState.NOT_RUNNING:
|
||||
return await self.async_step_start_otbr_addon()
|
||||
|
||||
# If the addon is already installed and running, fail
|
||||
return self.async_abort(
|
||||
reason="otbr_addon_already_running",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": otbr_manager.addon_name,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_install_otbr_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show progress dialog for installing the OTBR addon."""
|
||||
return await self._install_addon(
|
||||
get_otbr_addon_manager(self.hass), "install_otbr_addon", "start_otbr_addon"
|
||||
)
|
||||
|
||||
async def async_step_start_otbr_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Configure OTBR to point to the SkyConnect and run the addon."""
|
||||
otbr_manager = get_otbr_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(otbr_manager)
|
||||
|
||||
assert self._device is not None
|
||||
new_addon_config = {
|
||||
**addon_info.options,
|
||||
"device": self._device,
|
||||
"baudrate": 460800,
|
||||
"flow_control": True,
|
||||
"autoflash_firmware": True,
|
||||
}
|
||||
|
||||
_LOGGER.debug("Reconfiguring OTBR addon with %s", new_addon_config)
|
||||
await self._async_set_addon_config(new_addon_config, otbr_manager)
|
||||
|
||||
if not self.addon_start_task:
|
||||
# Before we start the addon, confirm that the correct firmware is running
|
||||
# and populate `self._probed_firmware_info` with the correct information
|
||||
if not await self._probe_firmware_info(
|
||||
probe_methods=(ApplicationType.SPINEL,)
|
||||
):
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
addon_info = await self._async_get_addon_info(otbr_manager)
|
||||
|
||||
assert self._device is not None
|
||||
new_addon_config = {
|
||||
**addon_info.options,
|
||||
"device": self._device,
|
||||
"baudrate": 460800,
|
||||
"flow_control": True,
|
||||
"autoflash_firmware": False,
|
||||
}
|
||||
|
||||
_LOGGER.debug("Reconfiguring OTBR addon with %s", new_addon_config)
|
||||
|
||||
try:
|
||||
await otbr_manager.async_set_addon_options(new_addon_config)
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
raise AbortFlow(
|
||||
"addon_set_config_failed",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": otbr_manager.addon_name,
|
||||
},
|
||||
) from err
|
||||
|
||||
self.addon_start_task = self.hass.async_create_task(
|
||||
otbr_manager.async_start_addon_waiting()
|
||||
)
|
||||
@@ -417,14 +475,20 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Confirm OTBR setup."""
|
||||
assert self._device is not None
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="confirm_otbr",
|
||||
if not await self._probe_firmware_info(probe_methods=(ApplicationType.SPINEL,)):
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
# OTBR discovery is done automatically via hassio
|
||||
return self._async_flow_finished()
|
||||
if user_input is not None:
|
||||
# OTBR discovery is done automatically via hassio
|
||||
return self._async_flow_finished()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm_otbr",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def _async_flow_finished(self) -> ConfigFlowResult:
|
||||
|
||||
@@ -10,6 +10,22 @@
|
||||
"pick_firmware_thread": "Thread"
|
||||
}
|
||||
},
|
||||
"install_zigbee_flasher_addon": {
|
||||
"title": "Installing flasher",
|
||||
"description": "Installing the Silicon Labs Flasher add-on."
|
||||
},
|
||||
"run_zigbee_flasher_addon": {
|
||||
"title": "Installing Zigbee firmware",
|
||||
"description": "Installing Zigbee firmware. This will take about a minute."
|
||||
},
|
||||
"uninstall_zigbee_flasher_addon": {
|
||||
"title": "Removing flasher",
|
||||
"description": "Removing the Silicon Labs Flasher add-on."
|
||||
},
|
||||
"zigbee_flasher_failed": {
|
||||
"title": "Zigbee installation failed",
|
||||
"description": "The Zigbee firmware installation process was unsuccessful. Ensure no other software is trying to communicate with the {model} and try again."
|
||||
},
|
||||
"confirm_zigbee": {
|
||||
"title": "Zigbee setup complete",
|
||||
"description": "Your {model} is now a Zigbee coordinator and will be shown as discovered by the Zigbee Home Automation integration."
|
||||
@@ -39,7 +55,9 @@
|
||||
"unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or add-on is currently trying to communicate with the device. If you are running Home Assistant OS in a virtual machine or in Docker, please make sure that permissions are set correctly for the device."
|
||||
},
|
||||
"progress": {
|
||||
"install_firmware": "Please wait while {firmware_name} firmware is installed to your {model}, this will take a few minutes. Do not make any changes to your hardware or software until this finishes."
|
||||
"install_zigbee_flasher_addon": "The Silicon Labs Flasher add-on is installed, this may take a few minutes.",
|
||||
"run_zigbee_flasher_addon": "Please wait while Zigbee firmware is installed to your {model}, this will take a few minutes. Do not make any changes to your hardware or software until this finishes.",
|
||||
"uninstall_zigbee_flasher_addon": "The Silicon Labs Flasher add-on is being removed."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -92,6 +110,16 @@
|
||||
"data": {
|
||||
"disable_multi_pan": "Disable multiprotocol support"
|
||||
}
|
||||
},
|
||||
"install_flasher_addon": {
|
||||
"title": "The Silicon Labs Flasher add-on installation has started"
|
||||
},
|
||||
"configure_flasher_addon": {
|
||||
"title": "The Silicon Labs Flasher add-on installation has started"
|
||||
},
|
||||
"start_flasher_addon": {
|
||||
"title": "Installing firmware",
|
||||
"description": "Zigbee firmware is now being installed. This will take a few minutes."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import AsyncIterator, Callable
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from ha_silabs_firmware_client import FirmwareManifest, FirmwareMetadata
|
||||
from universal_silabs_flasher.firmware import parse_firmware_image
|
||||
from universal_silabs_flasher.flasher import Flasher
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.update import (
|
||||
@@ -17,12 +20,18 @@ from homeassistant.components.update import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.restore_state import ExtraStoredData
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import FirmwareUpdateCoordinator
|
||||
from .helpers import async_register_firmware_info_callback
|
||||
from .util import ApplicationType, FirmwareInfo, async_flash_silabs_firmware
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
guess_firmware_info,
|
||||
probe_silabs_firmware_info,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -240,11 +249,19 @@ class BaseFirmwareUpdateEntity(
|
||||
self._attr_update_percentage = round((offset * 100) / total_size)
|
||||
self.async_write_ha_state()
|
||||
|
||||
# Switch to an indeterminate progress bar after installation is complete, since
|
||||
# we probe the firmware after flashing
|
||||
if offset == total_size:
|
||||
self._attr_update_percentage = None
|
||||
self.async_write_ha_state()
|
||||
@asynccontextmanager
|
||||
async def _temporarily_stop_hardware_owners(
|
||||
self, device: str
|
||||
) -> AsyncIterator[None]:
|
||||
"""Temporarily stop addons and integrations communicating with the device."""
|
||||
firmware_info = await guess_firmware_info(self.hass, device)
|
||||
_LOGGER.debug("Identified firmware info: %s", firmware_info)
|
||||
|
||||
async with AsyncExitStack() as stack:
|
||||
for owner in firmware_info.owners:
|
||||
await stack.enter_async_context(owner.temporarily_stop(self.hass))
|
||||
|
||||
yield
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
@@ -261,18 +278,49 @@ class BaseFirmwareUpdateEntity(
|
||||
fw_data = await self.coordinator.client.async_fetch_firmware(
|
||||
self._latest_firmware
|
||||
)
|
||||
fw_image = await self.hass.async_add_executor_job(parse_firmware_image, fw_data)
|
||||
|
||||
try:
|
||||
firmware_info = await async_flash_silabs_firmware(
|
||||
hass=self.hass,
|
||||
device=self._current_device,
|
||||
fw_data=fw_data,
|
||||
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
|
||||
bootloader_reset_type=self.bootloader_reset_type,
|
||||
progress_callback=self._update_progress,
|
||||
)
|
||||
finally:
|
||||
self._attr_in_progress = False
|
||||
self.async_write_ha_state()
|
||||
device = self._current_device
|
||||
|
||||
self._firmware_info_callback(firmware_info)
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=(
|
||||
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
|
||||
ApplicationType.EZSP.as_flasher_application_type(),
|
||||
ApplicationType.SPINEL.as_flasher_application_type(),
|
||||
ApplicationType.CPC.as_flasher_application_type(),
|
||||
),
|
||||
bootloader_reset=self.bootloader_reset_type,
|
||||
)
|
||||
|
||||
async with self._temporarily_stop_hardware_owners(device):
|
||||
try:
|
||||
try:
|
||||
# Enter the bootloader with indeterminate progress
|
||||
await flasher.enter_bootloader()
|
||||
|
||||
# Flash the firmware, with progress
|
||||
await flasher.flash_firmware(
|
||||
fw_image, progress_callback=self._update_progress
|
||||
)
|
||||
except Exception as err:
|
||||
raise HomeAssistantError("Failed to flash firmware") from err
|
||||
|
||||
# Probe the running application type with indeterminate progress
|
||||
self._attr_update_percentage = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
firmware_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
probe_methods=(self.entity_description.expected_firmware_type,),
|
||||
)
|
||||
|
||||
if firmware_info is None:
|
||||
raise HomeAssistantError(
|
||||
"Failed to probe the firmware after flashing"
|
||||
)
|
||||
|
||||
self._firmware_info_callback(firmware_info)
|
||||
finally:
|
||||
self._attr_in_progress = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -4,20 +4,18 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
from collections.abc import AsyncIterator, Callable, Iterable
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from collections.abc import AsyncIterator, Iterable
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType
|
||||
from universal_silabs_flasher.firmware import parse_firmware_image
|
||||
from universal_silabs_flasher.flasher import Flasher
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
|
||||
@@ -335,52 +333,3 @@ async def probe_silabs_firmware_type(
|
||||
return None
|
||||
|
||||
return fw_info.firmware_type
|
||||
|
||||
|
||||
async def async_flash_silabs_firmware(
|
||||
hass: HomeAssistant,
|
||||
device: str,
|
||||
fw_data: bytes,
|
||||
expected_installed_firmware_type: ApplicationType,
|
||||
bootloader_reset_type: str | None = None,
|
||||
progress_callback: Callable[[int, int], None] | None = None,
|
||||
) -> FirmwareInfo:
|
||||
"""Flash firmware to the SiLabs device."""
|
||||
firmware_info = await guess_firmware_info(hass, device)
|
||||
_LOGGER.debug("Identified firmware info: %s", firmware_info)
|
||||
|
||||
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
|
||||
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=(
|
||||
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
|
||||
ApplicationType.EZSP.as_flasher_application_type(),
|
||||
ApplicationType.SPINEL.as_flasher_application_type(),
|
||||
ApplicationType.CPC.as_flasher_application_type(),
|
||||
),
|
||||
bootloader_reset=bootloader_reset_type,
|
||||
)
|
||||
|
||||
async with AsyncExitStack() as stack:
|
||||
for owner in firmware_info.owners:
|
||||
await stack.enter_async_context(owner.temporarily_stop(hass))
|
||||
|
||||
try:
|
||||
# Enter the bootloader with indeterminate progress
|
||||
await flasher.enter_bootloader()
|
||||
|
||||
# Flash the firmware, with progress
|
||||
await flasher.flash_firmware(fw_image, progress_callback=progress_callback)
|
||||
except Exception as err:
|
||||
raise HomeAssistantError("Failed to flash firmware") from err
|
||||
|
||||
probed_firmware_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
probe_methods=(expected_installed_firmware_type,),
|
||||
)
|
||||
|
||||
if probed_firmware_info is None:
|
||||
raise HomeAssistantError("Failed to probe the firmware after flashing")
|
||||
|
||||
return probed_firmware_info
|
||||
|
||||
@@ -32,7 +32,6 @@ from .const import (
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
PID,
|
||||
PRODUCT,
|
||||
SERIAL_NUMBER,
|
||||
@@ -46,29 +45,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class FirmwareInstallFlowProtocol(Protocol):
|
||||
"""Protocol describing `BaseFirmwareInstallFlow` for a mixin."""
|
||||
class TranslationPlaceholderProtocol(Protocol):
|
||||
"""Protocol describing `BaseFirmwareInstallFlow`'s translation placeholders."""
|
||||
|
||||
def _get_translation_placeholders(self) -> dict[str, str]:
|
||||
return {}
|
||||
|
||||
async def _install_firmware_step(
|
||||
self,
|
||||
fw_update_url: str,
|
||||
fw_type: str,
|
||||
firmware_name: str,
|
||||
expected_installed_firmware_type: ApplicationType,
|
||||
step_id: str,
|
||||
next_step_id: str,
|
||||
) -> ConfigFlowResult: ...
|
||||
|
||||
else:
|
||||
# Multiple inheritance with `Protocol` seems to break
|
||||
FirmwareInstallFlowProtocol = object
|
||||
TranslationPlaceholderProtocol = object
|
||||
|
||||
|
||||
class SkyConnectFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
"""Mixin for Home Assistant SkyConnect firmware methods."""
|
||||
class SkyConnectTranslationMixin(ConfigEntryBaseFlow, TranslationPlaceholderProtocol):
|
||||
"""Translation placeholder mixin for Home Assistant SkyConnect."""
|
||||
|
||||
context: ConfigFlowContext
|
||||
|
||||
@@ -83,35 +72,9 @@ class SkyConnectFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
|
||||
return placeholders
|
||||
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Install Zigbee firmware."""
|
||||
return await self._install_firmware_step(
|
||||
fw_update_url=NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
fw_type="skyconnect_zigbee_ncp",
|
||||
firmware_name="Zigbee",
|
||||
expected_installed_firmware_type=ApplicationType.EZSP,
|
||||
step_id="install_zigbee_firmware",
|
||||
next_step_id="confirm_zigbee",
|
||||
)
|
||||
|
||||
async def async_step_install_thread_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Install Thread firmware."""
|
||||
return await self._install_firmware_step(
|
||||
fw_update_url=NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
fw_type="skyconnect_openthread_rcp",
|
||||
firmware_name="OpenThread",
|
||||
expected_installed_firmware_type=ApplicationType.SPINEL,
|
||||
step_id="install_thread_firmware",
|
||||
next_step_id="start_otbr_addon",
|
||||
)
|
||||
|
||||
|
||||
class HomeAssistantSkyConnectConfigFlow(
|
||||
SkyConnectFirmwareMixin,
|
||||
SkyConnectTranslationMixin,
|
||||
firmware_config_flow.BaseFirmwareConfigFlow,
|
||||
domain=DOMAIN,
|
||||
):
|
||||
@@ -244,7 +207,7 @@ class HomeAssistantSkyConnectMultiPanOptionsFlowHandler(
|
||||
|
||||
|
||||
class HomeAssistantSkyConnectOptionsFlowHandler(
|
||||
SkyConnectFirmwareMixin, firmware_config_flow.BaseFirmwareOptionsFlow
|
||||
SkyConnectTranslationMixin, firmware_config_flow.BaseFirmwareOptionsFlow
|
||||
):
|
||||
"""Zigbee and Thread options flow handlers."""
|
||||
|
||||
|
||||
@@ -48,6 +48,16 @@
|
||||
"disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]"
|
||||
}
|
||||
},
|
||||
"install_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_flasher_addon::title%]"
|
||||
},
|
||||
"configure_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::configure_flasher_addon::title%]"
|
||||
},
|
||||
"start_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::description%]"
|
||||
},
|
||||
"pick_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]",
|
||||
@@ -56,6 +66,18 @@
|
||||
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]"
|
||||
}
|
||||
},
|
||||
"install_zigbee_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::description%]"
|
||||
},
|
||||
"run_zigbee_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::description%]"
|
||||
},
|
||||
"zigbee_flasher_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::description%]"
|
||||
},
|
||||
"confirm_zigbee": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
@@ -98,7 +120,9 @@
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
"install_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_zigbee_flasher_addon%]",
|
||||
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
|
||||
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
@@ -112,6 +136,22 @@
|
||||
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]"
|
||||
}
|
||||
},
|
||||
"install_zigbee_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::description%]"
|
||||
},
|
||||
"run_zigbee_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::description%]"
|
||||
},
|
||||
"uninstall_zigbee_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::uninstall_zigbee_flasher_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::uninstall_zigbee_flasher_addon::description%]"
|
||||
},
|
||||
"zigbee_flasher_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::description%]"
|
||||
},
|
||||
"confirm_zigbee": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
@@ -151,7 +191,9 @@
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
"install_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_zigbee_flasher_addon%]",
|
||||
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
|
||||
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol, final
|
||||
from typing import Any, final
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
@@ -31,7 +31,6 @@ from homeassistant.components.homeassistant_hardware.util import (
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_HARDWARE,
|
||||
ConfigEntry,
|
||||
ConfigEntryBaseFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
@@ -42,7 +41,6 @@ from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
RADIO_DEVICE,
|
||||
ZHA_DOMAIN,
|
||||
ZHA_HW_DISCOVERY_DATA,
|
||||
@@ -59,59 +57,8 @@ STEP_HW_SETTINGS_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class FirmwareInstallFlowProtocol(Protocol):
|
||||
"""Protocol describing `BaseFirmwareInstallFlow` for a mixin."""
|
||||
|
||||
async def _install_firmware_step(
|
||||
self,
|
||||
fw_update_url: str,
|
||||
fw_type: str,
|
||||
firmware_name: str,
|
||||
expected_installed_firmware_type: ApplicationType,
|
||||
step_id: str,
|
||||
next_step_id: str,
|
||||
) -> ConfigFlowResult: ...
|
||||
|
||||
else:
|
||||
# Multiple inheritance with `Protocol` seems to break
|
||||
FirmwareInstallFlowProtocol = object
|
||||
|
||||
|
||||
class YellowFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
"""Mixin for Home Assistant Yellow firmware methods."""
|
||||
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Install Zigbee firmware."""
|
||||
return await self._install_firmware_step(
|
||||
fw_update_url=NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
fw_type="yellow_zigbee_ncp",
|
||||
firmware_name="Zigbee",
|
||||
expected_installed_firmware_type=ApplicationType.EZSP,
|
||||
step_id="install_zigbee_firmware",
|
||||
next_step_id="confirm_zigbee",
|
||||
)
|
||||
|
||||
async def async_step_install_thread_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Install Thread firmware."""
|
||||
return await self._install_firmware_step(
|
||||
fw_update_url=NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
fw_type="yellow_openthread_rcp",
|
||||
firmware_name="OpenThread",
|
||||
expected_installed_firmware_type=ApplicationType.SPINEL,
|
||||
step_id="install_thread_firmware",
|
||||
next_step_id="start_otbr_addon",
|
||||
)
|
||||
|
||||
|
||||
class HomeAssistantYellowConfigFlow(
|
||||
YellowFirmwareMixin, BaseFirmwareConfigFlow, domain=DOMAIN
|
||||
):
|
||||
class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Home Assistant Yellow."""
|
||||
|
||||
VERSION = 1
|
||||
@@ -328,9 +275,7 @@ class HomeAssistantYellowMultiPanOptionsFlowHandler(
|
||||
|
||||
|
||||
class HomeAssistantYellowOptionsFlowHandler(
|
||||
YellowFirmwareMixin,
|
||||
BaseHomeAssistantYellowOptionsFlow,
|
||||
BaseFirmwareOptionsFlow,
|
||||
BaseHomeAssistantYellowOptionsFlow, BaseFirmwareOptionsFlow
|
||||
):
|
||||
"""Handle a firmware options flow for Home Assistant Yellow."""
|
||||
|
||||
|
||||
@@ -71,6 +71,16 @@
|
||||
"disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]"
|
||||
}
|
||||
},
|
||||
"install_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_flasher_addon::title%]"
|
||||
},
|
||||
"configure_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::configure_flasher_addon::title%]"
|
||||
},
|
||||
"start_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::description%]"
|
||||
},
|
||||
"pick_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]",
|
||||
@@ -79,6 +89,18 @@
|
||||
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]"
|
||||
}
|
||||
},
|
||||
"install_zigbee_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::description%]"
|
||||
},
|
||||
"run_zigbee_flasher_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::description%]"
|
||||
},
|
||||
"zigbee_flasher_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::description%]"
|
||||
},
|
||||
"confirm_zigbee": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
@@ -123,7 +145,9 @@
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
"install_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_zigbee_flasher_addon%]",
|
||||
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
|
||||
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -15,14 +15,12 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from .const import LOGGER
|
||||
from .coordinator import HusqvarnaCoordinator
|
||||
|
||||
type HusqvarnaConfigEntry = ConfigEntry[HusqvarnaCoordinator]
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.LAWN_MOWER,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: HusqvarnaConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Husqvarna Autoconnect Bluetooth from a config entry."""
|
||||
address = entry.data[CONF_ADDRESS]
|
||||
channel_id = entry.data[CONF_CLIENT_ID]
|
||||
@@ -56,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HusqvarnaConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: HusqvarnaConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
coordinator: HusqvarnaCoordinator = entry.runtime_data
|
||||
|
||||
@@ -3,31 +3,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from automower_ble.mower import Mower
|
||||
from bleak import BleakError
|
||||
from bleak_retry_connector import close_stale_connections_by_address
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import HusqvarnaConfigEntry
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
|
||||
class HusqvarnaCoordinator(DataUpdateCoordinator[dict[str, bytes]]):
|
||||
"""Class to manage fetching data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: HusqvarnaConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
mower: Mower,
|
||||
address: str,
|
||||
channel_id: str,
|
||||
|
||||
@@ -10,10 +10,10 @@ from homeassistant.components.lawn_mower import (
|
||||
LawnMowerEntity,
|
||||
LawnMowerEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HusqvarnaConfigEntry
|
||||
from .const import LOGGER
|
||||
from .coordinator import HusqvarnaCoordinator
|
||||
from .entity import HusqvarnaAutomowerBleEntity
|
||||
@@ -21,11 +21,11 @@ from .entity import HusqvarnaAutomowerBleEntity
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HusqvarnaConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AutomowerLawnMower integration from a config entry."""
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator: HusqvarnaCoordinator = config_entry.runtime_data
|
||||
address = coordinator.address
|
||||
|
||||
async_add_entities(
|
||||
|
||||
@@ -7,6 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ista_ecotrend",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyecotrend_ista"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["pyecotrend-ista==3.3.1"]
|
||||
}
|
||||
|
||||
@@ -50,18 +50,14 @@ rules:
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: The integration is a web service, there are no discoverable devices.
|
||||
docs-data-update: done
|
||||
docs-examples:
|
||||
status: done
|
||||
comment: describes how to use the integration with the statistics dashboard
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: changes are very rare (usually takes years)
|
||||
dynamic-devices: todo
|
||||
entity-category:
|
||||
status: done
|
||||
comment: The default category is appropriate.
|
||||
@@ -71,12 +67,8 @@ rules:
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: integration has no repairs
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: integration has no stale devices
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
|
||||
@@ -8,6 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pypck"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pypck==0.8.9", "lcn-frontend==0.2.5"]
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: Integration has no configuration parameters
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration has no authentication.
|
||||
test-coverage: done
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: done
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device discovery has to be manually triggered in LCN. Manually adding devices is implemented.
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: |
|
||||
Since all entities are configured manually, they are enabled by default.
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: |
|
||||
Since all entities are configured manually, names are user-defined.
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues: done
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device discovery has to be manually triggered in LCN. Manually removing devices is implemented.
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration is not making any HTTP requests.
|
||||
strict-typing: todo
|
||||
@@ -46,9 +46,6 @@
|
||||
"motor_fault_short": "mdi:flash-off",
|
||||
"motor_ot_amps": "mdi:flash-alert"
|
||||
}
|
||||
},
|
||||
"total_cycles": {
|
||||
"default": "mdi:counter"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pylitterbot==2024.2.1"]
|
||||
"requirements": ["pylitterbot==2024.2.0"]
|
||||
}
|
||||
|
||||
@@ -115,14 +115,6 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = {
|
||||
lambda robot: status.lower() if (status := robot.status_code) else None
|
||||
),
|
||||
),
|
||||
RobotSensorEntityDescription[LitterRobot](
|
||||
key="total_cycles",
|
||||
translation_key="total_cycles",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda robot: robot.cycle_count,
|
||||
),
|
||||
],
|
||||
LitterRobot4: [
|
||||
RobotSensorEntityDescription[LitterRobot4](
|
||||
|
||||
@@ -118,10 +118,6 @@
|
||||
"spf": "Pinch detect at startup"
|
||||
}
|
||||
},
|
||||
"total_cycles": {
|
||||
"name": "Total cycles",
|
||||
"unit_of_measurement": "cycles"
|
||||
},
|
||||
"waste_drawer": {
|
||||
"name": "Waste drawer"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -10,18 +11,19 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import CONF_CALENDAR_NAME, CONF_STORAGE_KEY, STORAGE_PATH
|
||||
from .const import CONF_CALENDAR_NAME, CONF_STORAGE_KEY, DOMAIN, STORAGE_PATH
|
||||
from .store import LocalCalendarStore
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.CALENDAR]
|
||||
|
||||
type LocalCalendarConfigEntry = ConfigEntry[LocalCalendarStore]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: LocalCalendarConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Local Calendar from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
if CONF_STORAGE_KEY not in entry.data:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
@@ -38,23 +40,22 @@ async def async_setup_entry(
|
||||
except OSError as err:
|
||||
raise ConfigEntryNotReady("Failed to load file {path}: {err}") from err
|
||||
|
||||
entry.runtime_data = store
|
||||
hass.data[DOMAIN][entry.entry_id] = store
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: LocalCalendarConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_remove_entry(
|
||||
hass: HomeAssistant, entry: LocalCalendarConfigEntry
|
||||
) -> None:
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle removal of an entry."""
|
||||
key = slugify(entry.data[CONF_CALENDAR_NAME])
|
||||
path = Path(hass.config.path(STORAGE_PATH.format(key=key)))
|
||||
|
||||
@@ -23,13 +23,13 @@ from homeassistant.components.calendar import (
|
||||
CalendarEntityFeature,
|
||||
CalendarEvent,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import LocalCalendarConfigEntry
|
||||
from .const import CONF_CALENDAR_NAME
|
||||
from .const import CONF_CALENDAR_NAME, DOMAIN
|
||||
from .store import LocalCalendarStore
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -39,11 +39,11 @@ PRODID = "-//homeassistant.io//local_calendar 1.0//EN"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: LocalCalendarConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the local calendar platform."""
|
||||
store = config_entry.runtime_data
|
||||
store = hass.data[DOMAIN][config_entry.entry_id]
|
||||
ics = await store.async_load()
|
||||
calendar: Calendar = await hass.async_add_executor_job(
|
||||
IcsCalendarStream.calendar_from_ics, ics
|
||||
|
||||
@@ -5,14 +5,15 @@ from typing import Any
|
||||
|
||||
from ical.diagnostics import redact_ics
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import LocalCalendarConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: LocalCalendarConfigEntry
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
payload: dict[str, Any] = {
|
||||
@@ -20,7 +21,7 @@ async def async_get_config_entry_diagnostics(
|
||||
"timezone": str(dt_util.get_default_time_zone()),
|
||||
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo),
|
||||
}
|
||||
store = config_entry.runtime_data
|
||||
store = hass.data[DOMAIN][config_entry.entry_id]
|
||||
ics = await store.async_load()
|
||||
payload["ics"] = "\n".join(redact_ics(ics))
|
||||
return payload
|
||||
|
||||
@@ -19,6 +19,7 @@ from aiolookin import (
|
||||
)
|
||||
from aiolookin.models import UDPCommandType, UDPEvent
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
@@ -33,7 +34,7 @@ from .const import (
|
||||
TYPE_TO_PLATFORM,
|
||||
)
|
||||
from .coordinator import LookinDataUpdateCoordinator, LookinPushCoordinator
|
||||
from .models import LookinConfigEntry, LookinData
|
||||
from .models import LookinData
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -90,7 +91,7 @@ class LookinUDPManager:
|
||||
self._subscriptions = None
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LookinConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up lookin from a config entry."""
|
||||
domain_data = hass.data.setdefault(DOMAIN, {})
|
||||
host = entry.data[CONF_HOST]
|
||||
@@ -171,7 +172,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LookinConfigEntry) -> bo
|
||||
)
|
||||
)
|
||||
|
||||
entry.runtime_data = LookinData(
|
||||
hass.data[DOMAIN][entry.entry_id] = LookinData(
|
||||
host=host,
|
||||
lookin_udp_subs=lookin_udp_subs,
|
||||
lookin_device=lookin_device,
|
||||
@@ -186,9 +187,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: LookinConfigEntry) -> bo
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: LookinConfigEntry) -> bool:
|
||||
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)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
manager: LookinUDPManager = hass.data[DOMAIN][UDP_MANAGER]
|
||||
@@ -197,7 +199,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: LookinConfigEntry) -> b
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, entry: LookinConfigEntry, device_entry: dr.DeviceEntry
|
||||
hass: HomeAssistant, entry: ConfigEntry, device_entry: dr.DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove lookin config entry from a device."""
|
||||
data: LookinData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
PRECISION_WHOLE,
|
||||
@@ -29,10 +30,10 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import TYPE_TO_PLATFORM
|
||||
from .const import DOMAIN, TYPE_TO_PLATFORM
|
||||
from .coordinator import LookinDataUpdateCoordinator
|
||||
from .entity import LookinCoordinatorEntity
|
||||
from .models import LookinConfigEntry, LookinData
|
||||
from .models import LookinData
|
||||
|
||||
LOOKIN_FAN_MODE_IDX_TO_HASS: Final = [FAN_AUTO, FAN_LOW, FAN_MIDDLE, FAN_HIGH]
|
||||
LOOKIN_SWING_MODE_IDX_TO_HASS: Final = [SWING_OFF, SWING_BOTH]
|
||||
@@ -63,11 +64,11 @@ LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: LookinConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the climate platform for lookin from a config entry."""
|
||||
lookin_data = config_entry.runtime_data
|
||||
lookin_data: LookinData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entities = []
|
||||
|
||||
for remote in lookin_data.devices:
|
||||
|
||||
@@ -6,16 +6,13 @@ from collections.abc import Awaitable, Callable
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import NEVER_TIME, POLLING_FALLBACK_SECONDS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .models import LookinConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -47,12 +44,12 @@ class LookinPushCoordinator:
|
||||
class LookinDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
"""DataUpdateCoordinator to gather data for a specific lookin devices."""
|
||||
|
||||
config_entry: LookinConfigEntry
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: LookinConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
push_coordinator: LookinPushCoordinator,
|
||||
name: str,
|
||||
update_interval: timedelta | None = None,
|
||||
|
||||
@@ -6,24 +6,25 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import TYPE_TO_PLATFORM
|
||||
from .const import DOMAIN, TYPE_TO_PLATFORM
|
||||
from .entity import LookinPowerPushRemoteEntity
|
||||
from .models import LookinConfigEntry
|
||||
from .models import LookinData
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: LookinConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the light platform for lookin from a config entry."""
|
||||
lookin_data = config_entry.runtime_data
|
||||
lookin_data: LookinData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entities = []
|
||||
|
||||
for remote in lookin_data.devices:
|
||||
|
||||
@@ -12,14 +12,15 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import TYPE_TO_PLATFORM
|
||||
from .const import DOMAIN, TYPE_TO_PLATFORM
|
||||
from .coordinator import LookinDataUpdateCoordinator
|
||||
from .entity import LookinPowerPushRemoteEntity
|
||||
from .models import LookinConfigEntry, LookinData
|
||||
from .models import LookinData
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,11 +43,11 @@ _FUNCTION_NAME_TO_FEATURE = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: LookinConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the media_player platform for lookin from a config entry."""
|
||||
lookin_data = config_entry.runtime_data
|
||||
lookin_data: LookinData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
entities = []
|
||||
|
||||
for remote in lookin_data.devices:
|
||||
|
||||
@@ -13,12 +13,8 @@ from aiolookin import (
|
||||
Remote,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from .coordinator import LookinDataUpdateCoordinator
|
||||
|
||||
type LookinConfigEntry = ConfigEntry[LookinData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LookinData:
|
||||
|
||||
@@ -10,12 +10,14 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import LookinDeviceCoordinatorEntity
|
||||
from .models import LookinConfigEntry, LookinData
|
||||
from .models import LookinData
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,11 +42,11 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: LookinConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up lookin sensors from the config entry."""
|
||||
lookin_data = config_entry.runtime_data
|
||||
lookin_data: LookinData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
if lookin_data.lookin_device.model >= 2:
|
||||
async_add_entities(
|
||||
|
||||
@@ -2,22 +2,28 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
import aiohttp
|
||||
from loqedAPI import loqed
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import LoqedConfigEntry, LoqedDataCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LoqedDataCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.LOCK, Platform.SENSOR]
|
||||
PLATFORMS: list[str] = [Platform.LOCK, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LoqedConfigEntry) -> bool:
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up loqed from a config entry."""
|
||||
websession = async_get_clientsession(hass)
|
||||
host = entry.data["bridge_ip"]
|
||||
@@ -43,15 +49,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: LoqedConfigEntry) -> boo
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: LoqedConfigEntry) -> bool:
|
||||
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)
|
||||
await entry.runtime_data.remove_webhooks()
|
||||
coordinator: LoqedDataCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
await coordinator.remove_webhooks()
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -17,8 +17,6 @@ from .const import CONF_CLOUDHOOK_URL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type LoqedConfigEntry = ConfigEntry[LoqedDataCoordinator]
|
||||
|
||||
|
||||
class BatteryMessage(TypedDict):
|
||||
"""Properties in a battery update message."""
|
||||
@@ -73,12 +71,12 @@ class StatusMessage(TypedDict):
|
||||
class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]):
|
||||
"""Data update coordinator for the loqed platform."""
|
||||
|
||||
config_entry: LoqedConfigEntry
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: LoqedConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
api: loqed.LoqedAPI,
|
||||
lock: loqed.Lock,
|
||||
) -> None:
|
||||
@@ -168,9 +166,7 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]):
|
||||
await self.lock.deleteWebhook(webhook_index)
|
||||
|
||||
|
||||
async def async_cloudhook_generate_url(
|
||||
hass: HomeAssistant, entry: LoqedConfigEntry
|
||||
) -> str:
|
||||
async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str:
|
||||
"""Generate the full URL for a webhook_id."""
|
||||
if CONF_CLOUDHOOK_URL not in entry.data:
|
||||
webhook_url = await cloud.async_create_cloudhook(
|
||||
|
||||
@@ -6,10 +6,12 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.lock import LockEntity, LockEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import LoqedConfigEntry, LoqedDataCoordinator
|
||||
from . import LoqedDataCoordinator
|
||||
from .const import DOMAIN
|
||||
from .entity import LoqedEntity
|
||||
|
||||
WEBHOOK_API_ENDPOINT = "/api/loqed/webhook"
|
||||
@@ -19,11 +21,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LoqedConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Loqed lock platform."""
|
||||
async_add_entities([LoqedLock(entry.runtime_data)])
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities([LoqedLock(coordinator)])
|
||||
|
||||
|
||||
class LoqedLock(LoqedEntity, LockEntity):
|
||||
|
||||
@@ -8,6 +8,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
@@ -16,7 +17,8 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import LoqedConfigEntry, LoqedDataCoordinator, StatusMessage
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LoqedDataCoordinator, StatusMessage
|
||||
from .entity import LoqedEntity
|
||||
|
||||
SENSORS: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
@@ -41,11 +43,11 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LoqedConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Loqed lock platform."""
|
||||
coordinator = entry.runtime_data
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(LoqedSensor(coordinator, sensor) for sensor in SENSORS)
|
||||
|
||||
|
||||
@@ -6,18 +6,25 @@ the integration name.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from luftdaten import Luftdaten
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from luftdaten import Luftdaten
|
||||
from luftdaten.exceptions import LuftdatenError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_SENSOR_ID
|
||||
from .coordinator import LuftdatenConfigEntry, LuftdatenDataUpdateCoordinator
|
||||
from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LuftdatenConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Sensor.Community as config entry."""
|
||||
|
||||
# For backwards compat, set unique ID
|
||||
@@ -28,15 +35,38 @@ async def async_setup_entry(hass: HomeAssistant, entry: LuftdatenConfigEntry) ->
|
||||
|
||||
sensor_community = Luftdaten(entry.data[CONF_SENSOR_ID])
|
||||
|
||||
coordinator = LuftdatenDataUpdateCoordinator(hass, entry, sensor_community)
|
||||
async def async_update() -> dict[str, float | int]:
|
||||
"""Update sensor/binary sensor data."""
|
||||
try:
|
||||
await sensor_community.get_data()
|
||||
except LuftdatenError as err:
|
||||
raise UpdateFailed("Unable to retrieve data from Sensor.Community") from err
|
||||
|
||||
if not sensor_community.values:
|
||||
raise UpdateFailed("Did not receive sensor data from Sensor.Community")
|
||||
|
||||
data: dict[str, float | int] = sensor_community.values
|
||||
data.update(sensor_community.meta)
|
||||
return data
|
||||
|
||||
coordinator: DataUpdateCoordinator[dict[str, Any]] = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"{DOMAIN}_{sensor_community.sensor_id}",
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
update_method=async_update,
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: LuftdatenConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload an Sensor.Community config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
del hass.data[DOMAIN][entry.entry_id]
|
||||
return unload_ok
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
"""Support for Sensor.Community stations.
|
||||
|
||||
Sensor.Community was previously called Luftdaten, hence the domain differs from
|
||||
the integration name.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from luftdaten import Luftdaten
|
||||
from luftdaten.exceptions import LuftdatenError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type LuftdatenConfigEntry = ConfigEntry[LuftdatenDataUpdateCoordinator]
|
||||
|
||||
|
||||
class LuftdatenDataUpdateCoordinator(DataUpdateCoordinator[dict[str, float | int]]):
|
||||
"""Data update coordinator for Sensor.Community."""
|
||||
|
||||
config_entry: LuftdatenConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: LuftdatenConfigEntry,
|
||||
sensor_community: Luftdaten,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"{DOMAIN}_{sensor_community.sensor_id}",
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
self._sensor_community = sensor_community
|
||||
|
||||
async def _async_update_data(self) -> dict[str, float | int]:
|
||||
"""Update sensor/binary sensor data."""
|
||||
try:
|
||||
await self._sensor_community.get_data()
|
||||
except LuftdatenError as err:
|
||||
raise UpdateFailed("Unable to retrieve data from Sensor.Community") from err
|
||||
|
||||
if not self._sensor_community.values:
|
||||
raise UpdateFailed("Did not receive sensor data from Sensor.Community")
|
||||
|
||||
data: dict[str, float | int] = self._sensor_community.values
|
||||
data.update(self._sensor_community.meta)
|
||||
return data
|
||||
@@ -5,11 +5,12 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import CONF_SENSOR_ID
|
||||
from .coordinator import LuftdatenConfigEntry
|
||||
from .const import CONF_SENSOR_ID, DOMAIN
|
||||
|
||||
TO_REDACT = {
|
||||
CONF_LATITUDE,
|
||||
@@ -19,8 +20,10 @@ TO_REDACT = {
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: LuftdatenConfigEntry
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
coordinator: DataUpdateCoordinator[dict[str, Any]] = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
return async_redact_data(coordinator.data, TO_REDACT)
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
@@ -22,10 +23,12 @@ from homeassistant.const import (
|
||||
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 homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import ATTR_SENSOR_ID, CONF_SENSOR_ID, DOMAIN
|
||||
from .coordinator import LuftdatenConfigEntry, LuftdatenDataUpdateCoordinator
|
||||
|
||||
SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
@@ -70,11 +73,11 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LuftdatenConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a Sensor.Community sensor based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
SensorCommunitySensor(
|
||||
@@ -98,7 +101,7 @@ class SensorCommunitySensor(CoordinatorEntity, SensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
coordinator: LuftdatenDataUpdateCoordinator,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
sensor_id: int,
|
||||
show_on_map: bool,
|
||||
|
||||
@@ -12,6 +12,8 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "lupusec"
|
||||
|
||||
NOTIFICATION_ID = "lupusec_notification"
|
||||
NOTIFICATION_TITLE = "Lupusec Security Setup"
|
||||
|
||||
@@ -22,10 +24,8 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
type LupusecConfigEntry = ConfigEntry[lupupy.Lupusec]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LupusecConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
|
||||
host = entry.data[CONF_HOST]
|
||||
@@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LupusecConfigEntry) -> b
|
||||
_LOGGER.error("Failed to connect to Lupusec device at %s", host)
|
||||
return False
|
||||
|
||||
entry.runtime_data = lupusec_system
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = lupusec_system
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import LupusecConfigEntry
|
||||
from .const import DOMAIN
|
||||
from . import DOMAIN
|
||||
from .entity import LupusecDevice
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=2)
|
||||
@@ -24,11 +24,11 @@ SCAN_INTERVAL = timedelta(seconds=2)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: LupusecConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up an alarm control panel for a Lupusec device."""
|
||||
data = config_entry.runtime_data
|
||||
data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
alarm = await hass.async_add_executor_job(data.get_alarm)
|
||||
|
||||
|
||||
@@ -12,10 +12,11 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import LupusecConfigEntry
|
||||
from . import DOMAIN
|
||||
from .entity import LupusecBaseSensor
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=2)
|
||||
@@ -25,12 +26,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: LupusecConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a binary sensors for a Lupusec device."""
|
||||
|
||||
data = config_entry.runtime_data
|
||||
data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
device_types = CONST.TYPE_OPENING + CONST.TYPE_SENSOR
|
||||
|
||||
|
||||
@@ -9,10 +9,11 @@ from typing import Any
|
||||
import lupupy.constants as CONST
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import LupusecConfigEntry
|
||||
from . import DOMAIN
|
||||
from .entity import LupusecBaseSensor
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=2)
|
||||
@@ -20,12 +21,12 @@ SCAN_INTERVAL = timedelta(seconds=2)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: LupusecConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Lupusec switch devices."""
|
||||
|
||||
data = config_entry.runtime_data
|
||||
data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
device_types = CONST.TYPE_SWITCH
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from aiolyric import Lyric
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import (
|
||||
@@ -18,14 +19,14 @@ from .api import (
|
||||
OAuth2SessionLyric,
|
||||
)
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LyricConfigEntry, LyricDataUpdateCoordinator
|
||||
from .coordinator import LyricDataUpdateCoordinator
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LyricConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Honeywell Lyric from a config entry."""
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
@@ -52,13 +53,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: LyricConfigEntry) -> boo
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: LyricConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -24,6 +24,7 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
PRECISION_HALVES,
|
||||
@@ -37,6 +38,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
LYRIC_EXCEPTIONS,
|
||||
PRESET_HOLD_UNTIL,
|
||||
PRESET_NO_HOLD,
|
||||
@@ -44,7 +46,7 @@ from .const import (
|
||||
PRESET_TEMPORARY_HOLD,
|
||||
PRESET_VACATION_HOLD,
|
||||
)
|
||||
from .coordinator import LyricConfigEntry, LyricDataUpdateCoordinator
|
||||
from .coordinator import LyricDataUpdateCoordinator
|
||||
from .entity import LyricDeviceEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -119,11 +121,11 @@ SCHEMA_HOLD_TIME: VolDictType = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LyricConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Honeywell Lyric climate platform based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
coordinator: LyricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
(
|
||||
|
||||
@@ -20,18 +20,16 @@ from .api import OAuth2SessionLyric
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type LyricConfigEntry = ConfigEntry[LyricDataUpdateCoordinator]
|
||||
|
||||
|
||||
class LyricDataUpdateCoordinator(DataUpdateCoordinator[Lyric]):
|
||||
"""Data update coordinator for Honeywell Lyric."""
|
||||
|
||||
config_entry: LyricConfigEntry
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: LyricConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
oauth_session: OAuth2SessionLyric,
|
||||
lyric: Lyric,
|
||||
) -> None:
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -23,13 +24,14 @@ from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PRESET_HOLD_UNTIL,
|
||||
PRESET_NO_HOLD,
|
||||
PRESET_PERMANENT_HOLD,
|
||||
PRESET_TEMPORARY_HOLD,
|
||||
PRESET_VACATION_HOLD,
|
||||
)
|
||||
from .coordinator import LyricConfigEntry, LyricDataUpdateCoordinator
|
||||
from .coordinator import LyricDataUpdateCoordinator
|
||||
from .entity import LyricAccessoryEntity, LyricDeviceEntity
|
||||
|
||||
LYRIC_SETPOINT_STATUS_NAMES = {
|
||||
@@ -157,11 +159,11 @@ def get_datetime_from_future_time(time_str: str) -> datetime:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LyricConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Honeywell Lyric sensor platform based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
coordinator: LyricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
LyricSensor(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
@@ -74,11 +74,6 @@ OPERATIONAL_STATE_MAP = {
|
||||
clusters.OperationalState.Enums.OperationalStateEnum.kRunning: "running",
|
||||
clusters.OperationalState.Enums.OperationalStateEnum.kPaused: "paused",
|
||||
clusters.OperationalState.Enums.OperationalStateEnum.kError: "error",
|
||||
}
|
||||
|
||||
RVC_OPERATIONAL_STATE_MAP = {
|
||||
# enum with known Operation state values which we can translate
|
||||
**OPERATIONAL_STATE_MAP,
|
||||
clusters.RvcOperationalState.Enums.OperationalStateEnum.kSeekingCharger: "seeking_charger",
|
||||
clusters.RvcOperationalState.Enums.OperationalStateEnum.kCharging: "charging",
|
||||
clusters.RvcOperationalState.Enums.OperationalStateEnum.kDocked: "docked",
|
||||
@@ -176,10 +171,6 @@ class MatterOperationalStateSensorEntityDescription(MatterSensorEntityDescriptio
|
||||
state_list_attribute: type[ClusterAttributeDescriptor] = (
|
||||
clusters.OperationalState.Attributes.OperationalStateList
|
||||
)
|
||||
state_attribute: type[ClusterAttributeDescriptor] = (
|
||||
clusters.OperationalState.Attributes.OperationalState
|
||||
)
|
||||
state_map: dict[int, str] = field(default_factory=lambda: OPERATIONAL_STATE_MAP)
|
||||
|
||||
|
||||
class MatterSensor(MatterEntity, SensorEntity):
|
||||
@@ -254,15 +245,15 @@ class MatterOperationalStateSensor(MatterSensor):
|
||||
for state in operational_state_list:
|
||||
# prefer translateable (known) state from mapping,
|
||||
# fallback to the raw state label as given by the device/manufacturer
|
||||
states_map[state.operationalStateID] = (
|
||||
self.entity_description.state_map.get(
|
||||
state.operationalStateID, slugify(state.operationalStateLabel)
|
||||
)
|
||||
states_map[state.operationalStateID] = OPERATIONAL_STATE_MAP.get(
|
||||
state.operationalStateID, slugify(state.operationalStateLabel)
|
||||
)
|
||||
self.states_map = states_map
|
||||
self._attr_options = list(states_map.values())
|
||||
self._attr_native_value = states_map.get(
|
||||
self.get_matter_attribute_value(self.entity_description.state_attribute)
|
||||
self.get_matter_attribute_value(
|
||||
clusters.OperationalState.Attributes.OperationalState
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1008,8 +999,6 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="operational_state",
|
||||
state_list_attribute=clusters.RvcOperationalState.Attributes.OperationalStateList,
|
||||
state_attribute=clusters.RvcOperationalState.Attributes.OperationalState,
|
||||
state_map=RVC_OPERATIONAL_STATE_MAP,
|
||||
),
|
||||
entity_class=MatterOperationalStateSensor,
|
||||
required_attributes=(
|
||||
@@ -1027,7 +1016,6 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="operational_state",
|
||||
state_list_attribute=clusters.OvenCavityOperationalState.Attributes.OperationalStateList,
|
||||
state_attribute=clusters.OvenCavityOperationalState.Attributes.OperationalState,
|
||||
),
|
||||
entity_class=MatterOperationalStateSensor,
|
||||
required_attributes=(
|
||||
|
||||
@@ -30,10 +30,10 @@ class OperationalState(IntEnum):
|
||||
Combination of generic OperationalState and RvcOperationalState.
|
||||
"""
|
||||
|
||||
STOPPED = 0x00
|
||||
RUNNING = 0x01
|
||||
PAUSED = 0x02
|
||||
ERROR = 0x03
|
||||
NO_ERROR = 0x00
|
||||
UNABLE_TO_START_OR_RESUME = 0x01
|
||||
UNABLE_TO_COMPLETE_OPERATION = 0x02
|
||||
COMMAND_INVALID_IN_STATE = 0x03
|
||||
SEEKING_CHARGER = 0x40
|
||||
CHARGING = 0x41
|
||||
DOCKED = 0x42
|
||||
@@ -95,7 +95,7 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
|
||||
async def async_pause(self) -> None:
|
||||
"""Pause the cleaning task."""
|
||||
await self.send_device_command(clusters.RvcOperationalState.Commands.Pause())
|
||||
await self.send_device_command(clusters.OperationalState.Commands.Pause())
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
@@ -120,10 +120,11 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
state = VacuumActivity.DOCKED
|
||||
elif operational_state == OperationalState.SEEKING_CHARGER:
|
||||
state = VacuumActivity.RETURNING
|
||||
elif operational_state == OperationalState.ERROR:
|
||||
elif operational_state in (
|
||||
OperationalState.UNABLE_TO_COMPLETE_OPERATION,
|
||||
OperationalState.UNABLE_TO_START_OR_RESUME,
|
||||
):
|
||||
state = VacuumActivity.ERROR
|
||||
elif operational_state == OperationalState.PAUSED:
|
||||
state = VacuumActivity.PAUSED
|
||||
elif (run_mode := self._supported_run_modes.get(run_mode_raw)) is not None:
|
||||
tags = {x.value for x in run_mode.modeTags}
|
||||
if ModeTag.CLEANING in tags:
|
||||
@@ -200,7 +201,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_class=MatterVacuum,
|
||||
required_attributes=(
|
||||
clusters.RvcRunMode.Attributes.CurrentMode,
|
||||
clusters.RvcOperationalState.Attributes.OperationalState,
|
||||
clusters.RvcOperationalState.Attributes.CurrentPhase,
|
||||
),
|
||||
optional_attributes=(
|
||||
clusters.RvcCleanMode.Attributes.CurrentMode,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user