mirror of
https://github.com/home-assistant/core.git
synced 2025-11-04 08:29:37 +00:00
Compare commits
4 Commits
rc
...
matter-err
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08eee8d479 | ||
|
|
6bbaae7235 | ||
|
|
86a5dff3f5 | ||
|
|
34e137005d |
2
Dockerfile
generated
2
Dockerfile
generated
@@ -31,7 +31,7 @@ RUN \
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.9.6
|
||||
RUN pip3 install uv==0.9.5
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airthings-ble==1.2.0"]
|
||||
"requirements": ["airthings-ble==1.1.1"]
|
||||
}
|
||||
|
||||
@@ -26,6 +26,3 @@ COUNTRY_DOMAINS = {
|
||||
"us": DEFAULT_DOMAIN,
|
||||
"za": "co.za",
|
||||
}
|
||||
|
||||
CATEGORY_SENSORS = "sensors"
|
||||
CATEGORY_NOTIFICATIONS = "notifications"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==6.5.6"]
|
||||
"requirements": ["aioamazondevices==6.4.6"]
|
||||
}
|
||||
|
||||
@@ -4,15 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
from aioamazondevices.const import (
|
||||
NOTIFICATION_ALARM,
|
||||
NOTIFICATION_REMINDER,
|
||||
NOTIFICATION_TIMER,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -25,7 +19,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import CATEGORY_NOTIFICATIONS, CATEGORY_SENSORS
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
|
||||
@@ -43,20 +36,6 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
|
||||
and (sensor := device.sensors.get(key)) is not None
|
||||
and sensor.error is False
|
||||
)
|
||||
category: str = CATEGORY_SENSORS
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonNotificationEntityDescription(SensorEntityDescription):
|
||||
"""Amazon Devices notification entity description."""
|
||||
|
||||
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
|
||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
||||
device.online
|
||||
and (notification := device.notifications.get(key)) is not None
|
||||
and notification.next_occurrence is not None
|
||||
)
|
||||
category: str = CATEGORY_NOTIFICATIONS
|
||||
|
||||
|
||||
SENSORS: Final = (
|
||||
@@ -77,23 +56,6 @@ SENSORS: Final = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
NOTIFICATIONS: Final = (
|
||||
AmazonNotificationEntityDescription(
|
||||
key=NOTIFICATION_ALARM,
|
||||
translation_key="alarm",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
AmazonNotificationEntityDescription(
|
||||
key=NOTIFICATION_REMINDER,
|
||||
translation_key="reminder",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
AmazonNotificationEntityDescription(
|
||||
key=NOTIFICATION_TIMER,
|
||||
translation_key="timer",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -112,18 +74,12 @@ async def async_setup_entry(
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
sensors_list = [
|
||||
async_add_entities(
|
||||
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in SENSORS
|
||||
for serial_num in new_devices
|
||||
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
|
||||
]
|
||||
notifications_list = [
|
||||
AmazonSensorEntity(coordinator, serial_num, notification_desc)
|
||||
for notification_desc in NOTIFICATIONS
|
||||
for serial_num in new_devices
|
||||
]
|
||||
async_add_entities(sensors_list + notifications_list)
|
||||
)
|
||||
|
||||
_check_device()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
@@ -132,9 +88,7 @@ async def async_setup_entry(
|
||||
class AmazonSensorEntity(AmazonEntity, SensorEntity):
|
||||
"""Sensor device."""
|
||||
|
||||
entity_description: (
|
||||
AmazonSensorEntityDescription | AmazonNotificationEntityDescription
|
||||
)
|
||||
entity_description: AmazonSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
@@ -147,13 +101,9 @@ class AmazonSensorEntity(AmazonEntity, SensorEntity):
|
||||
return super().native_unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime:
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
# Sensors
|
||||
if self.entity_description.category == CATEGORY_SENSORS:
|
||||
return self.device.sensors[self.entity_description.key].value
|
||||
# Notifications
|
||||
return self.device.notifications[self.entity_description.key].next_occurrence
|
||||
return self.device.sensors[self.entity_description.key].value
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -66,17 +66,6 @@
|
||||
"name": "Speak"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"alarm": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
"reminder": {
|
||||
"name": "Next reminder"
|
||||
},
|
||||
"timer": {
|
||||
"name": "Next timer"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.4.0"]
|
||||
"requirements": ["hassil==3.3.0"]
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ class BryantEvolutionClimate(ClimateEntity):
|
||||
return HVACAction.HEATING
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="failed_to_parse_hvac_action",
|
||||
translation_key="failed_to_parse_hvac_mode",
|
||||
translation_placeholders={
|
||||
"mode_and_active": mode_and_active,
|
||||
"current_temperature": str(self.current_temperature),
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"failed_to_parse_hvac_action": {
|
||||
"message": "Could not determine HVAC action: {mode_and_active}, {current_temperature}, {target_temperature_low}"
|
||||
"message": "Could not determine HVAC action: {mode_and_active}, {self.current_temperature}, {self.target_temperature_low}"
|
||||
},
|
||||
"failed_to_parse_hvac_mode": {
|
||||
"message": "Cannot parse response to HVACMode: {mode}"
|
||||
|
||||
@@ -115,37 +115,26 @@ GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend(
|
||||
{vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA}}
|
||||
)
|
||||
|
||||
_BASE_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_COGNITO_CLIENT_ID): str,
|
||||
vol.Optional(CONF_USER_POOL_ID): str,
|
||||
vol.Optional(CONF_REGION): str,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
||||
vol.Optional(CONF_ACCOUNT_LINK_SERVER): str,
|
||||
vol.Optional(CONF_ACCOUNTS_SERVER): str,
|
||||
vol.Optional(CONF_ACME_SERVER): str,
|
||||
vol.Optional(CONF_API_SERVER): str,
|
||||
vol.Optional(CONF_RELAYER_SERVER): str,
|
||||
vol.Optional(CONF_REMOTESTATE_SERVER): str,
|
||||
vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Any(
|
||||
_BASE_CONFIG_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_MODE): vol.In([MODE_DEV]),
|
||||
vol.Required(CONF_API_SERVER): str,
|
||||
}
|
||||
),
|
||||
_BASE_CONFIG_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In([MODE_PROD]),
|
||||
}
|
||||
),
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(
|
||||
[MODE_DEV, MODE_PROD]
|
||||
),
|
||||
vol.Optional(CONF_COGNITO_CLIENT_ID): str,
|
||||
vol.Optional(CONF_USER_POOL_ID): str,
|
||||
vol.Optional(CONF_REGION): str,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
||||
vol.Optional(CONF_ACCOUNT_LINK_SERVER): str,
|
||||
vol.Optional(CONF_ACCOUNTS_SERVER): str,
|
||||
vol.Optional(CONF_ACME_SERVER): str,
|
||||
vol.Optional(CONF_API_SERVER): str,
|
||||
vol.Optional(CONF_RELAYER_SERVER): str,
|
||||
vol.Optional(CONF_REMOTESTATE_SERVER): str,
|
||||
vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.5.1"],
|
||||
"requirements": ["hass-nabucasa==1.4.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -768,16 +768,7 @@ class DefaultAgent(ConversationEntity):
|
||||
if lang_intents.fuzzy_matcher is None:
|
||||
return None
|
||||
|
||||
context_area: str | None = None
|
||||
satellite_area, _ = self._get_satellite_area_and_device(
|
||||
user_input.satellite_id, user_input.device_id
|
||||
)
|
||||
if satellite_area:
|
||||
context_area = satellite_area.name
|
||||
|
||||
fuzzy_result = lang_intents.fuzzy_matcher.match(
|
||||
user_input.text, context_area=context_area
|
||||
)
|
||||
fuzzy_result = lang_intents.fuzzy_matcher.match(user_input.text)
|
||||
if fuzzy_result is None:
|
||||
return None
|
||||
|
||||
@@ -1249,14 +1240,15 @@ class DefaultAgent(ConversationEntity):
|
||||
intent_slot_list_names=self._fuzzy_config.slot_list_names,
|
||||
slot_combinations={
|
||||
intent_name: {
|
||||
combo_key: SlotCombinationInfo(
|
||||
context_area=combo_info.context_area,
|
||||
name_domains=(
|
||||
set(combo_info.name_domains)
|
||||
if combo_info.name_domains
|
||||
else None
|
||||
),
|
||||
)
|
||||
combo_key: [
|
||||
SlotCombinationInfo(
|
||||
name_domains=(
|
||||
set(combo_info.name_domains)
|
||||
if combo_info.name_domains
|
||||
else None
|
||||
)
|
||||
)
|
||||
]
|
||||
for combo_key, combo_info in intent_combos.items()
|
||||
}
|
||||
for intent_name, intent_combos in self._fuzzy_config.slot_combinations.items()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.4.0", "home-assistant-intents==2025.10.28"]
|
||||
"requirements": ["hassil==3.3.0", "home-assistant-intents==2025.10.28"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pycync==0.4.3"]
|
||||
"requirements": ["pycync==0.4.2"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==16.3.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==16.1.0"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eheimdigital"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["eheimdigital==1.4.0"],
|
||||
"requirements": ["eheimdigital==1.3.0"],
|
||||
"zeroconf": [
|
||||
{ "name": "eheimdigital._http._tcp.local.", "type": "_http._tcp.local." }
|
||||
]
|
||||
|
||||
@@ -40,9 +40,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
|
||||
client = Firefly(
|
||||
api_url=data[CONF_URL],
|
||||
api_key=data[CONF_API_KEY],
|
||||
session=async_get_clientsession(
|
||||
hass=hass, verify_ssl=data[CONF_VERIFY_SSL]
|
||||
),
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
await client.get_about()
|
||||
except FireflyAuthenticationError:
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20251103.0"]
|
||||
"requirements": ["home-assistant-frontend==20251001.4"]
|
||||
}
|
||||
|
||||
@@ -620,11 +620,7 @@ 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)
|
||||
|
||||
# Unload coordinator
|
||||
coordinator: HassioDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
|
||||
coordinator.unload()
|
||||
|
||||
# Pop coordinator
|
||||
# Pop add-on data
|
||||
hass.data.pop(ADDONS_COORDINATOR, None)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -563,8 +563,3 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
self.async_set_updated_data(data)
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Could not refresh info for %s: %s", addon_slug, err)
|
||||
|
||||
@callback
|
||||
def unload(self) -> None:
|
||||
"""Clean up when config entry unloaded."""
|
||||
self.jobs.unload()
|
||||
|
||||
@@ -44,6 +44,7 @@ from .const import (
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
EXTRA_PLACEHOLDERS,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
ISSUE_KEY_ADDON_DEPRECATED,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_ADDON_PWNED,
|
||||
@@ -86,6 +87,7 @@ ISSUE_KEYS_FOR_REPAIRS = {
|
||||
"issue_system_disk_lifetime",
|
||||
ISSUE_KEY_SYSTEM_FREE_SPACE,
|
||||
ISSUE_KEY_ADDON_PWNED,
|
||||
ISSUE_KEY_ADDON_DEPRECATED,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, replace
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
@@ -30,8 +29,6 @@ from .const import (
|
||||
)
|
||||
from .handler import get_supervisor_client
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class JobSubscription:
|
||||
@@ -48,7 +45,7 @@ class JobSubscription:
|
||||
event_callback: Callable[[Job], Any]
|
||||
uuid: str | None = None
|
||||
name: str | None = None
|
||||
reference: str | None = None
|
||||
reference: str | None | type[Any] = Any
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate at least one filter option is present."""
|
||||
@@ -61,7 +58,7 @@ class JobSubscription:
|
||||
"""Return true if job matches subscription filters."""
|
||||
if self.uuid:
|
||||
return job.uuid == self.uuid
|
||||
return job.name == self.name and self.reference in (None, job.reference)
|
||||
return job.name == self.name and self.reference in (Any, job.reference)
|
||||
|
||||
|
||||
class SupervisorJobs:
|
||||
@@ -73,7 +70,6 @@ class SupervisorJobs:
|
||||
self._supervisor_client = get_supervisor_client(hass)
|
||||
self._jobs: dict[UUID, Job] = {}
|
||||
self._subscriptions: set[JobSubscription] = set()
|
||||
self._dispatcher_disconnect: Callable[[], None] | None = None
|
||||
|
||||
@property
|
||||
def current_jobs(self) -> list[Job]:
|
||||
@@ -83,24 +79,20 @@ class SupervisorJobs:
|
||||
def subscribe(self, subscription: JobSubscription) -> CALLBACK_TYPE:
|
||||
"""Subscribe to updates for job. Return callback is used to unsubscribe.
|
||||
|
||||
If any jobs match the subscription at the time this is called, runs the
|
||||
callback on them.
|
||||
If any jobs match the subscription at the time this is called, creates
|
||||
tasks to run their callback on it.
|
||||
"""
|
||||
self._subscriptions.add(subscription)
|
||||
|
||||
# Run the callback on each existing match
|
||||
# We catch all errors to prevent an error in one from stopping the others
|
||||
for match in [job for job in self._jobs.values() if subscription.matches(job)]:
|
||||
try:
|
||||
return subscription.event_callback(match)
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOGGER.error(
|
||||
"Error encountered processing Supervisor Job (%s %s %s) - %s",
|
||||
match.name,
|
||||
match.reference,
|
||||
match.uuid,
|
||||
err,
|
||||
)
|
||||
# As these are callbacks they are safe to run in the event loop
|
||||
# We wrap these in an asyncio task so subscribing does not wait on the logic
|
||||
if matches := [job for job in self._jobs.values() if subscription.matches(job)]:
|
||||
|
||||
async def event_callback_async(job: Job) -> Any:
|
||||
return subscription.event_callback(job)
|
||||
|
||||
for match in matches:
|
||||
self._hass.async_create_task(event_callback_async(match))
|
||||
|
||||
return partial(self._subscriptions.discard, subscription)
|
||||
|
||||
@@ -139,7 +131,7 @@ class SupervisorJobs:
|
||||
|
||||
# If this is the first update register to receive Supervisor events
|
||||
if first_update:
|
||||
self._dispatcher_disconnect = async_dispatcher_connect(
|
||||
async_dispatcher_connect(
|
||||
self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_jobs
|
||||
)
|
||||
|
||||
@@ -166,14 +158,3 @@ class SupervisorJobs:
|
||||
for sub in self._subscriptions:
|
||||
if sub.matches(job):
|
||||
sub.event_callback(job)
|
||||
|
||||
# If the job is done, pop it from our cache if present after processing is done
|
||||
if job.done and job.uuid in self._jobs:
|
||||
del self._jobs[job.uuid]
|
||||
|
||||
@callback
|
||||
def unload(self) -> None:
|
||||
"""Unregister with dispatcher on config entry unload."""
|
||||
if self._dispatcher_disconnect:
|
||||
self._dispatcher_disconnect()
|
||||
self._dispatcher_disconnect = None
|
||||
|
||||
@@ -2,35 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.usb import USBDevice, async_register_port_event_callback
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DEVICE, DOMAIN, NABU_CASA_FIRMWARE_RELEASES_URL
|
||||
from .const import DEVICE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type HomeAssistantConnectZBT2ConfigEntry = ConfigEntry[HomeAssistantConnectZBT2Data]
|
||||
|
||||
|
||||
@dataclass
|
||||
class HomeAssistantConnectZBT2Data:
|
||||
"""Runtime data definition."""
|
||||
|
||||
coordinator: FirmwareUpdateCoordinator
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@@ -64,9 +49,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: HomeAssistantConnectZBT2ConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant Connect ZBT-2 config entry."""
|
||||
|
||||
# Postpone loading the config entry if the device is missing
|
||||
@@ -77,23 +60,12 @@ async def async_setup_entry(
|
||||
translation_key="device_disconnected",
|
||||
)
|
||||
|
||||
# Create and store the firmware update coordinator in runtime_data
|
||||
session = async_get_clientsession(hass)
|
||||
coordinator = FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
)
|
||||
entry.runtime_data = HomeAssistantConnectZBT2Data(coordinator)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["switch", "update"])
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: HomeAssistantConnectZBT2ConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, ["switch", "update"])
|
||||
await hass.config_entries.async_unload_platforms(entry, ["update"])
|
||||
return True
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
DOMAIN = "homeassistant_connect_zbt2"
|
||||
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL = (
|
||||
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases"
|
||||
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases/latest"
|
||||
)
|
||||
|
||||
FIRMWARE = "firmware"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"default": "mdi:test-tube"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,13 +90,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"name": "Beta firmware updates"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"device_disconnected": {
|
||||
"message": "The device is not plugged in"
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
"""Home Assistant Connect ZBT-2 switch entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.switch import (
|
||||
BaseBetaFirmwareSwitch,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantConnectZBT2ConfigEntry
|
||||
from .const import DOMAIN, HARDWARE_NAME, SERIAL_NUMBER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeAssistantConnectZBT2ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform for Home Assistant Connect ZBT-2."""
|
||||
async_add_entities(
|
||||
[BetaFirmwareSwitch(config_entry.runtime_data.coordinator, config_entry)]
|
||||
)
|
||||
|
||||
|
||||
class BetaFirmwareSwitch(BaseBetaFirmwareSwitch):
|
||||
"""Home Assistant Connect ZBT-2 beta firmware switch."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: FirmwareUpdateCoordinator,
|
||||
config_entry: HomeAssistantConnectZBT2ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the beta firmware switch."""
|
||||
super().__init__(coordinator, config_entry)
|
||||
|
||||
serial_number = self._config_entry.data[SERIAL_NUMBER]
|
||||
|
||||
self._attr_unique_id = f"{serial_number}_beta_firmware"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
name=f"{HARDWARE_NAME} ({serial_number})",
|
||||
model=HARDWARE_NAME,
|
||||
manufacturer="Nabu Casa",
|
||||
serial_number=serial_number,
|
||||
)
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -17,14 +19,22 @@ from homeassistant.components.homeassistant_hardware.util import (
|
||||
ResetTarget,
|
||||
)
|
||||
from homeassistant.components.update import UpdateDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantConnectZBT2ConfigEntry
|
||||
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, HARDWARE_NAME, SERIAL_NUMBER
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
HARDWARE_NAME,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
SERIAL_NUMBER,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -81,7 +91,8 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
|
||||
def _async_create_update_entity(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeAssistantConnectZBT2ConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
session: aiohttp.ClientSession,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> FirmwareUpdateEntity:
|
||||
"""Create an update entity that handles firmware type changes."""
|
||||
@@ -100,7 +111,12 @@ def _async_create_update_entity(
|
||||
entity = FirmwareUpdateEntity(
|
||||
device=config_entry.data["device"],
|
||||
config_entry=config_entry,
|
||||
update_coordinator=config_entry.runtime_data.coordinator,
|
||||
update_coordinator=FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
),
|
||||
entity_description=entity_description,
|
||||
)
|
||||
|
||||
@@ -110,7 +126,11 @@ def _async_create_update_entity(
|
||||
"""Replace the current entity when the firmware type changes."""
|
||||
er.async_get(hass).async_remove(entity.entity_id)
|
||||
async_add_entities(
|
||||
[_async_create_update_entity(hass, config_entry, async_add_entities)]
|
||||
[
|
||||
_async_create_update_entity(
|
||||
hass, config_entry, session, async_add_entities
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
entity.async_on_remove(
|
||||
@@ -122,11 +142,14 @@ def _async_create_update_entity(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeAssistantConnectZBT2ConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the firmware update config entry."""
|
||||
entity = _async_create_update_entity(hass, config_entry, async_add_entities)
|
||||
session = async_get_clientsession(hass)
|
||||
entity = _async_create_update_entity(
|
||||
hass, config_entry, session, async_add_entities
|
||||
)
|
||||
|
||||
async_add_entities([entity])
|
||||
|
||||
@@ -139,7 +162,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
def __init__(
|
||||
self,
|
||||
device: str,
|
||||
config_entry: HomeAssistantConnectZBT2ConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
update_coordinator: FirmwareUpdateCoordinator,
|
||||
entity_description: FirmwareUpdateEntityDescription,
|
||||
) -> None:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
"universal-silabs-flasher==0.0.37",
|
||||
"ha-silabs-firmware-client==0.3.0"
|
||||
"universal-silabs-flasher==0.0.35",
|
||||
"ha-silabs-firmware-client==0.2.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"""Home Assistant Hardware base beta firmware switch entity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .coordinator import FirmwareUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseBetaFirmwareSwitch(SwitchEntity, RestoreEntity):
|
||||
"""Base switch to enable beta firmware updates."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_entity_registry_enabled_default = False
|
||||
_attr_translation_key = "beta_firmware"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: FirmwareUpdateCoordinator,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the beta firmware switch."""
|
||||
self._coordinator = coordinator
|
||||
self._config_entry = config_entry
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Restore the last state
|
||||
last_state = await self.async_get_last_state()
|
||||
if last_state is not None:
|
||||
self._attr_is_on = last_state.state == "on"
|
||||
else:
|
||||
self._attr_is_on = False
|
||||
|
||||
# Apply the restored state to the coordinator
|
||||
await self._update_coordinator_prerelease()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on beta firmware updates."""
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
await self._update_coordinator_prerelease()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off beta firmware updates."""
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
await self._update_coordinator_prerelease()
|
||||
|
||||
async def _update_coordinator_prerelease(self) -> None:
|
||||
"""Update the coordinator with the current prerelease setting."""
|
||||
self._coordinator.client.update_prerelease(bool(self._attr_is_on))
|
||||
await self._coordinator.async_refresh()
|
||||
@@ -150,11 +150,6 @@ class BaseFirmwareUpdateEntity(
|
||||
|
||||
self._update_attributes()
|
||||
|
||||
# Fetch firmware info early to avoid prolonged "unknown" state when the device
|
||||
# is initially set up
|
||||
if self._latest_manifest is None:
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
def extra_restore_state_data(self) -> FirmwareUpdateExtraStoredData:
|
||||
"""Return state data to be restored."""
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
||||
from homeassistant.components.usb import (
|
||||
USBDevice,
|
||||
@@ -19,7 +15,6 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
@@ -29,7 +24,6 @@ from .const import (
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
PID,
|
||||
PRODUCT,
|
||||
SERIAL_NUMBER,
|
||||
@@ -38,16 +32,6 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type HomeAssistantSkyConnectConfigEntry = ConfigEntry[HomeAssistantSkyConnectData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class HomeAssistantSkyConnectData:
|
||||
"""Runtime data definition."""
|
||||
|
||||
coordinator: FirmwareUpdateCoordinator
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@@ -81,9 +65,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: HomeAssistantSkyConnectConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant SkyConnect config entry."""
|
||||
|
||||
# Postpone loading the config entry if the device is missing
|
||||
@@ -94,31 +76,18 @@ async def async_setup_entry(
|
||||
translation_key="device_disconnected",
|
||||
)
|
||||
|
||||
# Create and store the firmware update coordinator in runtime_data
|
||||
session = async_get_clientsession(hass)
|
||||
coordinator = FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
)
|
||||
entry.runtime_data = HomeAssistantSkyConnectData(coordinator)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["switch", "update"])
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: HomeAssistantSkyConnectConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, ["switch", "update"])
|
||||
await hass.config_entries.async_unload_platforms(entry, ["update"])
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: HomeAssistantSkyConnectConfigEntry
|
||||
) -> bool:
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -8,7 +8,7 @@ DOMAIN = "homeassistant_sky_connect"
|
||||
DOCS_WEB_FLASHER_URL = "https://skyconnect.home-assistant.io/firmware-update/"
|
||||
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL = (
|
||||
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases"
|
||||
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases/latest"
|
||||
)
|
||||
|
||||
FIRMWARE = "firmware"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"default": "mdi:test-tube"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,13 +90,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"name": "Beta firmware updates"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"device_disconnected": {
|
||||
"message": "The device is not plugged in"
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
"""Home Assistant SkyConnect switch entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.switch import (
|
||||
BaseBetaFirmwareSwitch,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantSkyConnectConfigEntry
|
||||
from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, HardwareVariant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeAssistantSkyConnectConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform for Home Assistant SkyConnect."""
|
||||
async_add_entities(
|
||||
[BetaFirmwareSwitch(config_entry.runtime_data.coordinator, config_entry)]
|
||||
)
|
||||
|
||||
|
||||
class BetaFirmwareSwitch(BaseBetaFirmwareSwitch):
|
||||
"""Home Assistant SkyConnect beta firmware switch."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: FirmwareUpdateCoordinator,
|
||||
config_entry: HomeAssistantSkyConnectConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the beta firmware switch."""
|
||||
super().__init__(coordinator, config_entry)
|
||||
|
||||
variant = HardwareVariant.from_usb_product_name(
|
||||
self._config_entry.data[PRODUCT]
|
||||
)
|
||||
serial_number = self._config_entry.data[SERIAL_NUMBER]
|
||||
|
||||
self._attr_unique_id = f"{serial_number}_beta_firmware"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
name=f"{variant.full_name} ({serial_number[:8]})",
|
||||
model=variant.full_name,
|
||||
manufacturer="Nabu Casa",
|
||||
serial_number=serial_number,
|
||||
)
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -16,17 +18,19 @@ from homeassistant.components.homeassistant_hardware.util import (
|
||||
FirmwareInfo,
|
||||
)
|
||||
from homeassistant.components.update import UpdateDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantSkyConnectConfigEntry
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
PRODUCT,
|
||||
SERIAL_NUMBER,
|
||||
HardwareVariant,
|
||||
@@ -98,7 +102,8 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
|
||||
def _async_create_update_entity(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeAssistantSkyConnectConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
session: aiohttp.ClientSession,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> FirmwareUpdateEntity:
|
||||
"""Create an update entity that handles firmware type changes."""
|
||||
@@ -117,7 +122,12 @@ def _async_create_update_entity(
|
||||
entity = FirmwareUpdateEntity(
|
||||
device=config_entry.data["device"],
|
||||
config_entry=config_entry,
|
||||
update_coordinator=config_entry.runtime_data.coordinator,
|
||||
update_coordinator=FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
),
|
||||
entity_description=entity_description,
|
||||
)
|
||||
|
||||
@@ -127,7 +137,11 @@ def _async_create_update_entity(
|
||||
"""Replace the current entity when the firmware type changes."""
|
||||
er.async_get(hass).async_remove(entity.entity_id)
|
||||
async_add_entities(
|
||||
[_async_create_update_entity(hass, config_entry, async_add_entities)]
|
||||
[
|
||||
_async_create_update_entity(
|
||||
hass, config_entry, session, async_add_entities
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
entity.async_on_remove(
|
||||
@@ -139,11 +153,14 @@ def _async_create_update_entity(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeAssistantSkyConnectConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the firmware update config entry."""
|
||||
entity = _async_create_update_entity(hass, config_entry, async_add_entities)
|
||||
session = async_get_clientsession(hass)
|
||||
entity = _async_create_update_entity(
|
||||
hass, config_entry, session, async_add_entities
|
||||
)
|
||||
|
||||
async_add_entities([entity])
|
||||
|
||||
@@ -157,7 +174,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
def __init__(
|
||||
self,
|
||||
device: str,
|
||||
config_entry: HomeAssistantSkyConnectConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
update_coordinator: FirmwareUpdateCoordinator,
|
||||
entity_description: FirmwareUpdateEntityDescription,
|
||||
) -> None:
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from homeassistant.components.hassio import get_os_info
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
check_multi_pan_addon,
|
||||
)
|
||||
@@ -20,34 +16,14 @@ from homeassistant.config_entries import SOURCE_HARDWARE, ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import discovery_flow
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
|
||||
from .const import (
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
RADIO_DEVICE,
|
||||
ZHA_HW_DISCOVERY_DATA,
|
||||
)
|
||||
from .const import FIRMWARE, FIRMWARE_VERSION, RADIO_DEVICE, ZHA_HW_DISCOVERY_DATA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type HomeAssistantYellowConfigEntry = ConfigEntry[HomeAssistantYellowData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class HomeAssistantYellowData:
|
||||
"""Runtime data definition."""
|
||||
|
||||
coordinator: (
|
||||
FirmwareUpdateCoordinator # Type from homeassistant_hardware.coordinator
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: HomeAssistantYellowConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant Yellow config entry."""
|
||||
if not is_hassio(hass):
|
||||
# Not running under supervisor, Home Assistant may have been migrated
|
||||
@@ -80,31 +56,18 @@ async def async_setup_entry(
|
||||
data=ZHA_HW_DISCOVERY_DATA,
|
||||
)
|
||||
|
||||
# Create and store the firmware update coordinator in runtime_data
|
||||
session = async_get_clientsession(hass)
|
||||
coordinator = FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
)
|
||||
entry.runtime_data = HomeAssistantYellowData(coordinator)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["switch", "update"])
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: HomeAssistantYellowConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, ["switch", "update"])
|
||||
await hass.config_entries.async_unload_platforms(entry, ["update"])
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: HomeAssistantYellowConfigEntry
|
||||
) -> bool:
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -22,5 +22,5 @@ FIRMWARE_VERSION = "firmware_version"
|
||||
ZHA_DOMAIN = "zha"
|
||||
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL = (
|
||||
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases"
|
||||
"https://api.github.com/repos/NabuCasa/silabs-firmware-builder/releases/latest"
|
||||
)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"default": "mdi:test-tube"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"beta_firmware": {
|
||||
"name": "Radio beta firmware updates"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"radio_firmware": {
|
||||
"name": "Radio firmware"
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
"""Home Assistant Yellow switch entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.switch import (
|
||||
BaseBetaFirmwareSwitch,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantYellowConfigEntry
|
||||
from .const import DOMAIN, MANUFACTURER, MODEL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeAssistantYellowConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform for Home Assistant Yellow."""
|
||||
async_add_entities(
|
||||
[BetaFirmwareSwitch(config_entry.runtime_data.coordinator, config_entry)]
|
||||
)
|
||||
|
||||
|
||||
class BetaFirmwareSwitch(BaseBetaFirmwareSwitch):
|
||||
"""Home Assistant Yellow beta firmware switch."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: FirmwareUpdateCoordinator,
|
||||
config_entry: HomeAssistantYellowConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the beta firmware switch."""
|
||||
super().__init__(coordinator, config_entry)
|
||||
self._attr_unique_id = "beta_firmware"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, "yellow")},
|
||||
name=MODEL,
|
||||
model=MODEL,
|
||||
manufacturer=MANUFACTURER,
|
||||
)
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -17,14 +19,23 @@ from homeassistant.components.homeassistant_hardware.util import (
|
||||
ResetTarget,
|
||||
)
|
||||
from homeassistant.components.update import UpdateDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantYellowConfigEntry
|
||||
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, MANUFACTURER, MODEL, RADIO_DEVICE
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
MODEL,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
RADIO_DEVICE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -97,7 +108,8 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
|
||||
def _async_create_update_entity(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeAssistantYellowConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
session: aiohttp.ClientSession,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> FirmwareUpdateEntity:
|
||||
"""Create an update entity that handles firmware type changes."""
|
||||
@@ -116,7 +128,12 @@ def _async_create_update_entity(
|
||||
entity = FirmwareUpdateEntity(
|
||||
device=RADIO_DEVICE,
|
||||
config_entry=config_entry,
|
||||
update_coordinator=config_entry.runtime_data.coordinator,
|
||||
update_coordinator=FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
),
|
||||
entity_description=entity_description,
|
||||
)
|
||||
|
||||
@@ -126,7 +143,11 @@ def _async_create_update_entity(
|
||||
"""Replace the current entity when the firmware type changes."""
|
||||
er.async_get(hass).async_remove(entity.entity_id)
|
||||
async_add_entities(
|
||||
[_async_create_update_entity(hass, config_entry, async_add_entities)]
|
||||
[
|
||||
_async_create_update_entity(
|
||||
hass, config_entry, session, async_add_entities
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
entity.async_on_remove(
|
||||
@@ -138,11 +159,14 @@ def _async_create_update_entity(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeAssistantYellowConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the firmware update config entry."""
|
||||
entity = _async_create_update_entity(hass, config_entry, async_add_entities)
|
||||
session = async_get_clientsession(hass)
|
||||
entity = _async_create_update_entity(
|
||||
hass, config_entry, session, async_add_entities
|
||||
)
|
||||
|
||||
async_add_entities([entity])
|
||||
|
||||
@@ -155,7 +179,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
def __init__(
|
||||
self,
|
||||
device: str,
|
||||
config_entry: HomeAssistantYellowConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
update_coordinator: FirmwareUpdateCoordinator,
|
||||
entity_description: FirmwareUpdateEntityDescription,
|
||||
) -> None:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Base class for IOmeter entities."""
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -22,5 +21,4 @@ class IOmeterEntity(CoordinatorEntity[IOMeterCoordinator]):
|
||||
manufacturer="IOmeter GmbH",
|
||||
model="IOmeter",
|
||||
sw_version=coordinator.current_fw_version,
|
||||
configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}/",
|
||||
)
|
||||
|
||||
@@ -299,8 +299,8 @@ def _create_climate_ui(xknx: XKNX, conf: ConfigExtractor, name: str) -> XknxClim
|
||||
group_address_active_state=conf.get_state_and_passive(CONF_GA_ACTIVE),
|
||||
group_address_command_value_state=conf.get_state_and_passive(CONF_GA_VALVE),
|
||||
sync_state=sync_state,
|
||||
min_temp=conf.get(CONF_TARGET_TEMPERATURE, ClimateConf.MIN_TEMP),
|
||||
max_temp=conf.get(CONF_TARGET_TEMPERATURE, ClimateConf.MAX_TEMP),
|
||||
min_temp=conf.get(ClimateConf.MIN_TEMP),
|
||||
max_temp=conf.get(ClimateConf.MAX_TEMP),
|
||||
mode=climate_mode,
|
||||
group_address_fan_speed=conf.get_write(CONF_GA_FAN_SPEED),
|
||||
group_address_fan_speed_state=conf.get_state_and_passive(CONF_GA_FAN_SPEED),
|
||||
@@ -486,7 +486,7 @@ class _KnxClimate(ClimateEntity, _KnxEntityBase):
|
||||
ha_controller_modes.append(self._last_hvac_mode)
|
||||
ha_controller_modes.append(HVACMode.OFF)
|
||||
|
||||
hvac_modes = sorted(set(filter(None, ha_controller_modes)))
|
||||
hvac_modes = list(set(filter(None, ha_controller_modes)))
|
||||
return (
|
||||
hvac_modes
|
||||
if hvac_modes
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.10.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.10.31.195356"
|
||||
"knx-frontend==2025.10.26.81530"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/libre_hardware_monitor",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["librehardwaremonitor-api==1.5.0"]
|
||||
"requirements": ["librehardwaremonitor-api==1.4.0"]
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from librehardwaremonitor_api.model import LibreHardwareMonitorSensorData
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorStateClass
|
||||
@@ -53,10 +51,10 @@ class LibreHardwareMonitorSensor(
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_name: str = sensor_data.name
|
||||
self._attr_native_value: str | None = sensor_data.value
|
||||
self._attr_extra_state_attributes: dict[str, Any] = {
|
||||
STATE_MIN_VALUE: sensor_data.min,
|
||||
STATE_MAX_VALUE: sensor_data.max,
|
||||
self.value: str | None = sensor_data.value
|
||||
self._attr_extra_state_attributes: dict[str, str] = {
|
||||
STATE_MIN_VALUE: self._format_number_value(sensor_data.min),
|
||||
STATE_MAX_VALUE: self._format_number_value(sensor_data.max),
|
||||
}
|
||||
self._attr_native_unit_of_measurement = sensor_data.unit
|
||||
self._attr_unique_id: str = f"{entry_id}_{sensor_data.sensor_id}"
|
||||
@@ -74,12 +72,23 @@ class LibreHardwareMonitorSensor(
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if sensor_data := self.coordinator.data.sensor_data.get(self._sensor_id):
|
||||
self._attr_native_value = sensor_data.value
|
||||
self.value = sensor_data.value
|
||||
self._attr_extra_state_attributes = {
|
||||
STATE_MIN_VALUE: sensor_data.min,
|
||||
STATE_MAX_VALUE: sensor_data.max,
|
||||
STATE_MIN_VALUE: self._format_number_value(sensor_data.min),
|
||||
STATE_MAX_VALUE: self._format_number_value(sensor_data.max),
|
||||
}
|
||||
else:
|
||||
self._attr_native_value = None
|
||||
self.value = None
|
||||
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the formatted sensor value or None if no value is available."""
|
||||
if self.value is not None and self.value != "-":
|
||||
return self._format_number_value(self.value)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _format_number_value(number_str: str) -> str:
|
||||
return number_str.replace(",", ".")
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any
|
||||
from chip.clusters import Objects as clusters
|
||||
from chip.clusters.Objects import ClusterCommand, NullValue
|
||||
from matter_server.client.models import device_types
|
||||
from matter_server.common.errors import MatterError
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
@@ -18,6 +19,7 @@ from homeassistant.components.switch import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .entity import MatterEntity, MatterEntityDescription
|
||||
@@ -54,15 +56,21 @@ class MatterSwitch(MatterEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn switch on."""
|
||||
await self.send_device_command(
|
||||
clusters.OnOff.Commands.On(),
|
||||
)
|
||||
try:
|
||||
await self.send_device_command(
|
||||
clusters.OnOff.Commands.On(),
|
||||
)
|
||||
except MatterError as err:
|
||||
raise HomeAssistantError(f"Failed to set value: {err}") from err
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn switch off."""
|
||||
await self.send_device_command(
|
||||
clusters.OnOff.Commands.Off(),
|
||||
)
|
||||
try:
|
||||
await self.send_device_command(
|
||||
clusters.OnOff.Commands.Off(),
|
||||
)
|
||||
except MatterError as err:
|
||||
raise HomeAssistantError(f"Failed to set value: {err}") from err
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
@@ -83,18 +91,24 @@ class MatterGenericCommandSwitch(MatterSwitch):
|
||||
"""Turn switch on."""
|
||||
if self.entity_description.on_command:
|
||||
# custom command defined to set the new value
|
||||
await self.send_device_command(
|
||||
self.entity_description.on_command(),
|
||||
self.entity_description.command_timeout,
|
||||
)
|
||||
try:
|
||||
await self.send_device_command(
|
||||
self.entity_description.on_command(),
|
||||
self.entity_description.command_timeout,
|
||||
)
|
||||
except MatterError as err:
|
||||
raise HomeAssistantError(f"Failed to set value: {err}") from err
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn switch off."""
|
||||
if self.entity_description.off_command:
|
||||
await self.send_device_command(
|
||||
self.entity_description.off_command(),
|
||||
self.entity_description.command_timeout,
|
||||
)
|
||||
try:
|
||||
await self.send_device_command(
|
||||
self.entity_description.off_command(),
|
||||
self.entity_description.command_timeout,
|
||||
)
|
||||
except MatterError as err:
|
||||
raise HomeAssistantError(f"Failed to set value: {err}") from err
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
@@ -111,13 +125,16 @@ class MatterGenericCommandSwitch(MatterSwitch):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Send device command with timeout."""
|
||||
await self.matter_client.send_device_command(
|
||||
node_id=self._endpoint.node.node_id,
|
||||
endpoint_id=self._endpoint.endpoint_id,
|
||||
command=command,
|
||||
timed_request_timeout_ms=command_timeout,
|
||||
**kwargs,
|
||||
)
|
||||
try:
|
||||
await self.matter_client.send_device_command(
|
||||
node_id=self._endpoint.node.node_id,
|
||||
endpoint_id=self._endpoint.endpoint_id,
|
||||
command=command,
|
||||
timed_request_timeout_ms=command_timeout,
|
||||
**kwargs,
|
||||
)
|
||||
except MatterError as err:
|
||||
raise HomeAssistantError(f"Failed to set value: {err}") from err
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
REVOLUTIONS_PER_MINUTE,
|
||||
STATE_UNKNOWN,
|
||||
EntityCategory,
|
||||
UnitOfEnergy,
|
||||
UnitOfTemperature,
|
||||
@@ -761,35 +762,40 @@ class MieleSensor(MieleEntity, SensorEntity):
|
||||
class MieleRestorableSensor(MieleSensor, RestoreSensor):
|
||||
"""Representation of a Sensor whose internal state can be restored."""
|
||||
|
||||
_attr_native_value: StateType
|
||||
_last_value: StateType
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MieleDataUpdateCoordinator,
|
||||
device_id: str,
|
||||
description: MieleSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, device_id, description)
|
||||
self._last_value = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# recover last value from cache when adding entity
|
||||
last_data = await self.async_get_last_sensor_data()
|
||||
if last_data:
|
||||
self._attr_native_value = last_data.native_value # type: ignore[assignment]
|
||||
last_value = await self.async_get_last_state()
|
||||
if last_value and last_value.state != STATE_UNKNOWN:
|
||||
self._last_value = last_value.state
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor.
|
||||
"""Return the state of the sensor."""
|
||||
return self._last_value
|
||||
|
||||
It is necessary to override `native_value` to fall back to the default
|
||||
attribute-based implementation, instead of the function-based
|
||||
implementation in `MieleSensor`.
|
||||
"""
|
||||
return self._attr_native_value
|
||||
|
||||
def _update_native_value(self) -> None:
|
||||
"""Update the native value attribute of the sensor."""
|
||||
self._attr_native_value = self.entity_description.value_fn(self.device)
|
||||
def _update_last_value(self) -> None:
|
||||
"""Update the last value of the sensor."""
|
||||
self._last_value = self.entity_description.value_fn(self.device)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._update_native_value()
|
||||
self._update_last_value()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
|
||||
@@ -906,7 +912,7 @@ class MieleProgramIdSensor(MieleSensor):
|
||||
class MieleTimeSensor(MieleRestorableSensor):
|
||||
"""Representation of time sensors keeping state from cache."""
|
||||
|
||||
def _update_native_value(self) -> None:
|
||||
def _update_last_value(self) -> None:
|
||||
"""Update the last value of the sensor."""
|
||||
|
||||
current_value = self.entity_description.value_fn(self.device)
|
||||
@@ -917,9 +923,7 @@ class MieleTimeSensor(MieleRestorableSensor):
|
||||
current_status == StateStatus.PROGRAM_ENDED
|
||||
and self.entity_description.end_value_fn is not None
|
||||
):
|
||||
self._attr_native_value = self.entity_description.end_value_fn(
|
||||
self._attr_native_value
|
||||
)
|
||||
self._last_value = self.entity_description.end_value_fn(self._last_value)
|
||||
|
||||
# keep value when program ends if no function is specified
|
||||
elif current_status == StateStatus.PROGRAM_ENDED:
|
||||
@@ -927,11 +931,11 @@ class MieleTimeSensor(MieleRestorableSensor):
|
||||
|
||||
# force unknown when appliance is not working (some devices are keeping last value until a new cycle starts)
|
||||
elif current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE):
|
||||
self._attr_native_value = None
|
||||
self._last_value = None
|
||||
|
||||
# otherwise, cache value and return it
|
||||
else:
|
||||
self._attr_native_value = current_value
|
||||
self._last_value = current_value
|
||||
|
||||
|
||||
class MieleConsumptionSensor(MieleRestorableSensor):
|
||||
@@ -939,13 +943,13 @@ class MieleConsumptionSensor(MieleRestorableSensor):
|
||||
|
||||
_is_reporting: bool = False
|
||||
|
||||
def _update_native_value(self) -> None:
|
||||
def _update_last_value(self) -> None:
|
||||
"""Update the last value of the sensor."""
|
||||
current_value = self.entity_description.value_fn(self.device)
|
||||
current_status = StateStatus(self.device.state_status)
|
||||
last_value = (
|
||||
float(cast(str, self._attr_native_value))
|
||||
if self._attr_native_value is not None
|
||||
float(cast(str, self._last_value))
|
||||
if self._last_value is not None and self._last_value != STATE_UNKNOWN
|
||||
else 0
|
||||
)
|
||||
|
||||
@@ -959,7 +963,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
|
||||
StateStatus.SERVICE,
|
||||
):
|
||||
self._is_reporting = False
|
||||
self._attr_native_value = None
|
||||
self._last_value = None
|
||||
|
||||
# appliance might report the last value for consumption of previous cycle and it will report 0
|
||||
# only after a while, so it is necessary to force 0 until we see the 0 value coming from API, unless
|
||||
@@ -969,7 +973,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
|
||||
and not self._is_reporting
|
||||
and last_value > 0
|
||||
):
|
||||
self._attr_native_value = current_value
|
||||
self._last_value = current_value
|
||||
self._is_reporting = True
|
||||
|
||||
elif (
|
||||
@@ -978,12 +982,12 @@ class MieleConsumptionSensor(MieleRestorableSensor):
|
||||
and current_value is not None
|
||||
and cast(int, current_value) > 0
|
||||
):
|
||||
self._attr_native_value = 0
|
||||
self._last_value = 0
|
||||
|
||||
# keep value when program ends
|
||||
elif current_status == StateStatus.PROGRAM_ENDED:
|
||||
pass
|
||||
|
||||
else:
|
||||
self._attr_native_value = current_value
|
||||
self._last_value = current_value
|
||||
self._is_reporting = True
|
||||
|
||||
@@ -49,44 +49,6 @@ class NSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def _validate_api_key(self, api_key: str) -> dict[str, str]:
|
||||
"""Validate the API key by testing connection to NS API.
|
||||
|
||||
Returns a dict of errors, empty if validation successful.
|
||||
"""
|
||||
errors: dict[str, str] = {}
|
||||
client = NSAPI(api_key)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(client.get_stations)
|
||||
except HTTPError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except (RequestsConnectionError, Timeout):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception validating API key")
|
||||
errors["base"] = "unknown"
|
||||
return errors
|
||||
|
||||
def _is_api_key_already_configured(
|
||||
self, api_key: str, exclude_entry_id: str | None = None
|
||||
) -> dict[str, str]:
|
||||
"""Check if the API key is already configured in another entry.
|
||||
|
||||
Args:
|
||||
api_key: The API key to check.
|
||||
exclude_entry_id: Optional entry ID to exclude from the check.
|
||||
|
||||
Returns:
|
||||
A dict of errors, empty if not already configured.
|
||||
"""
|
||||
for entry in self._async_current_entries():
|
||||
if (
|
||||
entry.entry_id != exclude_entry_id
|
||||
and entry.data.get(CONF_API_KEY) == api_key
|
||||
):
|
||||
return {"base": "already_configured"}
|
||||
return {}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -94,7 +56,16 @@ class NSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(user_input)
|
||||
errors = await self._validate_api_key(user_input[CONF_API_KEY])
|
||||
client = NSAPI(user_input[CONF_API_KEY])
|
||||
try:
|
||||
await self.hass.async_add_executor_job(client.get_stations)
|
||||
except HTTPError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except (RequestsConnectionError, Timeout):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception validating API key")
|
||||
errors["base"] = "unknown"
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=INTEGRATION_TITLE,
|
||||
@@ -106,33 +77,6 @@ class NSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration to update the API key from the UI."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input is not None:
|
||||
# Check if this API key is already used by another entry
|
||||
errors = self._is_api_key_already_configured(
|
||||
user_input[CONF_API_KEY], exclude_entry_id=reconfigure_entry.entry_id
|
||||
)
|
||||
|
||||
if not errors:
|
||||
errors = await self._validate_api_key(user_input[CONF_API_KEY])
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
data_updates={CONF_API_KEY: user_input[CONF_API_KEY]},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle import from YAML configuration."""
|
||||
self._async_abort_entries_match({CONF_API_KEY: import_data[CONF_API_KEY]})
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "This API key is already configured for another entry.",
|
||||
"cannot_connect": "Could not connect to NS API. Check your API key.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::nederlandse_spoorwegen::config::step::user::data_description::api_key%]"
|
||||
},
|
||||
"description": "Update your Nederlandse Spoorwegen API key."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
|
||||
@@ -7,7 +7,6 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pynintendoparental import Authenticator
|
||||
from pynintendoparental.api import Api
|
||||
from pynintendoparental.exceptions import HttpException, InvalidSessionTokenException
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -15,7 +14,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import APP_SETUP_URL, CONF_SESSION_TOKEN, DOMAIN
|
||||
from .const import CONF_SESSION_TOKEN, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,9 +37,6 @@ class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
nintendo_api = Api(
|
||||
self.auth, self.hass.config.time_zone, self.hass.config.language
|
||||
)
|
||||
try:
|
||||
await self.auth.complete_login(
|
||||
self.auth, user_input[CONF_API_TOKEN], False
|
||||
@@ -52,24 +48,12 @@ class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
assert self.auth.account_id
|
||||
await self.async_set_unique_id(self.auth.account_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
try:
|
||||
if "base" not in errors:
|
||||
await nintendo_api.async_get_account_devices()
|
||||
except HttpException as err:
|
||||
if err.status_code == 404:
|
||||
return self.async_abort(
|
||||
reason="no_devices_found",
|
||||
description_placeholders={"more_info_url": APP_SETUP_URL},
|
||||
)
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
if "base" not in errors:
|
||||
return self.async_create_entry(
|
||||
title=self.auth.account_id,
|
||||
data={
|
||||
CONF_SESSION_TOKEN: self.auth.get_session_token,
|
||||
},
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=self.auth.account_id,
|
||||
data={
|
||||
CONF_SESSION_TOKEN: self.auth.get_session_token,
|
||||
},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
description_placeholders={"link": self.auth.login_url},
|
||||
|
||||
@@ -8,7 +8,4 @@ BEDTIME_ALARM_MIN = "16:00"
|
||||
BEDTIME_ALARM_MAX = "23:00"
|
||||
BEDTIME_ALARM_DISABLE = "00:00"
|
||||
|
||||
APP_SETUP_URL = (
|
||||
"https://www.nintendo.com/my/support/switch/parentalcontrols/app/setup.html"
|
||||
)
|
||||
ATTR_BONUS_TIME = "bonus_time"
|
||||
|
||||
@@ -6,10 +6,7 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pynintendoparental import Authenticator, NintendoParental
|
||||
from pynintendoparental.exceptions import (
|
||||
InvalidOAuthConfigurationException,
|
||||
NoDevicesFoundException,
|
||||
)
|
||||
from pynintendoparental.exceptions import InvalidOAuthConfigurationException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -27,8 +24,6 @@ UPDATE_INTERVAL = timedelta(seconds=60)
|
||||
class NintendoUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Nintendo data update coordinator."""
|
||||
|
||||
config_entry: NintendoParentalControlsConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
@@ -55,8 +50,3 @@ class NintendoUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
raise ConfigEntryError(
|
||||
err, translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from err
|
||||
except NoDevicesFoundException as err:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_devices_found",
|
||||
) from err
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynintendoparental"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynintendoparental==1.1.3"]
|
||||
"requirements": ["pynintendoparental==1.1.2"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"no_devices_found": "There are no devices paired with this Nintendo account, go to [Nintendo Support]({more_info_url}) for further assistance.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
@@ -68,9 +67,6 @@
|
||||
},
|
||||
"device_not_found": {
|
||||
"message": "Device not found."
|
||||
},
|
||||
"no_devices_found": {
|
||||
"message": "No Nintendo devices found for this account."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -14,7 +14,7 @@ from onedrive_personal_sdk.exceptions import (
|
||||
NotFoundError,
|
||||
OneDriveException,
|
||||
)
|
||||
from onedrive_personal_sdk.models.items import ItemUpdate
|
||||
from onedrive_personal_sdk.models.items import Item, ItemUpdate
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -202,7 +202,9 @@ async def _get_onedrive_client(
|
||||
)
|
||||
|
||||
|
||||
async def _handle_item_operation[T](func: Callable[[], Awaitable[T]], folder: str) -> T:
|
||||
async def _handle_item_operation(
|
||||
func: Callable[[], Awaitable[Item]], folder: str
|
||||
) -> Item:
|
||||
try:
|
||||
return await func()
|
||||
except NotFoundError:
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.0.15"]
|
||||
"requirements": ["onedrive-personal-sdk==0.0.14"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["openai==2.2.0", "python-open-router==0.3.2"]
|
||||
"requirements": ["openai==2.2.0", "python-open-router==0.3.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.15.9"]
|
||||
"requirements": ["opower==0.15.8"]
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the local authentication step via config flow."""
|
||||
errors = {}
|
||||
description_placeholders = {
|
||||
"somfy_developer_mode_docs": "https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode#getting-started"
|
||||
"somfy-developer-mode-docs": "https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode#getting-started"
|
||||
}
|
||||
|
||||
if user_input:
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"token": "Token generated by the app used to control your device.",
|
||||
"verify_ssl": "Verify the SSL certificate. Select this only if you are connecting via the hostname."
|
||||
},
|
||||
"description": "By activating the [Developer Mode of your TaHoma box]({somfy_developer_mode_docs}), you can authorize third-party software (like Home Assistant) to connect to it via your local network.\n\n1. Open the TaHoma By Somfy application on your device.\n2. Navigate to the Help & advanced features -> Advanced features menu in the application.\n3. Activate Developer Mode by tapping 7 times on the version number of your gateway (like 2025.1.4-11).\n4. Generate a token from the Developer Mode menu to authenticate your API calls.\n\n5. Enter the generated token below and update the host to include your Gateway PIN or the IP address of your gateway."
|
||||
"description": "By activating the [Developer Mode of your TaHoma box]({somfy-developer-mode-docs}), you can authorize third-party software (like Home Assistant) to connect to it via your local network.\n\n1. Open the TaHoma By Somfy application on your device.\n2. Navigate to the Help & advanced features -> Advanced features menu in the application.\n3. Activate Developer Mode by tapping 7 times on the version number of your gateway (like 2025.1.4-11).\n4. Generate a token from the Developer Mode menu to authenticate your API calls.\n\n5. Enter the generated token below and update the host to include your Gateway PIN or the IP address of your gateway."
|
||||
},
|
||||
"local_or_cloud": {
|
||||
"data": {
|
||||
|
||||
@@ -23,28 +23,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR]
|
||||
DATA_PRIVILEGED_KEY: HassKey[bool | None] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: PingConfigEntry) -> bool:
|
||||
"""Migrate old config entries."""
|
||||
if entry.version == 1 and entry.minor_version == 1:
|
||||
_LOGGER.debug("Migrating to minor version 2")
|
||||
|
||||
# Migrate device registry identifiers from homeassistant domain to ping domain
|
||||
registry = dr.async_get(hass)
|
||||
if (
|
||||
device := registry.async_get_device(
|
||||
identifiers={(HOMEASSISTANT_DOMAIN, entry.entry_id)}
|
||||
)
|
||||
) is not None and entry.entry_id in device.config_entries:
|
||||
registry.async_update_device(
|
||||
device_id=device.id,
|
||||
new_identifiers={(DOMAIN, entry.entry_id)},
|
||||
)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the ping integration."""
|
||||
hass.data[DATA_PRIVILEGED_KEY] = await _can_use_icmp_lib_with_privilege()
|
||||
@@ -54,6 +32,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PingConfigEntry) -> bool:
|
||||
"""Set up Ping (ICMP) from a config entry."""
|
||||
|
||||
# Migrate device registry identifiers from homeassistant domain to ping domain
|
||||
registry = dr.async_get(hass)
|
||||
if (
|
||||
device := registry.async_get_device(
|
||||
identifiers={(HOMEASSISTANT_DOMAIN, entry.entry_id)}
|
||||
)
|
||||
) is not None and entry.entry_id in device.config_entries:
|
||||
registry.async_update_device(
|
||||
device_id=device.id,
|
||||
new_identifiers={(DOMAIN, entry.entry_id)},
|
||||
)
|
||||
|
||||
privileged = hass.data[DATA_PRIVILEGED_KEY]
|
||||
|
||||
host: str = entry.options[CONF_HOST]
|
||||
|
||||
@@ -37,7 +37,6 @@ class PingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Ping."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
||||
@@ -10,12 +10,11 @@ from homeassistant.components.device_tracker import (
|
||||
ScannerEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import CONF_IMPORTED_BY, DOMAIN
|
||||
from .const import CONF_IMPORTED_BY
|
||||
from .coordinator import PingConfigEntry, PingUpdateCoordinator
|
||||
|
||||
|
||||
@@ -25,7 +24,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a Ping config entry."""
|
||||
async_add_entities([PingDeviceTracker(hass, entry, entry.runtime_data)])
|
||||
async_add_entities([PingDeviceTracker(entry, entry.runtime_data)])
|
||||
|
||||
|
||||
class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity):
|
||||
@@ -34,10 +33,7 @@ class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity)
|
||||
_last_seen: datetime | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: PingConfigEntry,
|
||||
coordinator: PingUpdateCoordinator,
|
||||
self, config_entry: PingConfigEntry, coordinator: PingUpdateCoordinator
|
||||
) -> None:
|
||||
"""Initialize the Ping device tracker."""
|
||||
super().__init__(coordinator)
|
||||
@@ -50,13 +46,6 @@ class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
device := dr.async_get(hass).async_get_device(
|
||||
identifiers={(DOMAIN, config_entry.entry_id)}
|
||||
)
|
||||
) is not None:
|
||||
self.device_entry = device
|
||||
|
||||
@property
|
||||
def ip_address(self) -> str:
|
||||
"""Return the primary ip address of the device."""
|
||||
|
||||
@@ -38,7 +38,9 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
client = Portainer(
|
||||
api_url=data[CONF_URL],
|
||||
api_key=data[CONF_API_TOKEN],
|
||||
session=async_get_clientsession(hass=hass, verify_ssl=data[CONF_VERIFY_SSL]),
|
||||
session=async_get_clientsession(
|
||||
hass=hass, verify_ssl=data.get(CONF_VERIFY_SSL, True)
|
||||
),
|
||||
)
|
||||
try:
|
||||
await client.get_endpoints()
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyportainer==1.0.12"]
|
||||
"requirements": ["pyportainer==1.0.11"]
|
||||
}
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.16.3"]
|
||||
"requirements": ["reolink-aio==0.16.2"]
|
||||
}
|
||||
|
||||
@@ -835,7 +835,6 @@
|
||||
"vehicle_type": {
|
||||
"name": "Vehicle type",
|
||||
"state": {
|
||||
"bus": "Bus",
|
||||
"motorcycle": "Motorcycle",
|
||||
"pickup_truck": "Pickup truck",
|
||||
"sedan": "Sedan",
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
|
||||
from sfrbox_api.bridge import SFRBox
|
||||
from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
@@ -15,10 +16,11 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN, PLATFORMS, PLATFORMS_WITH_AUTH
|
||||
from .coordinator import SFRConfigEntry, SFRDataUpdateCoordinator, SFRRuntimeData
|
||||
from .coordinator import SFRDataUpdateCoordinator
|
||||
from .models import DomainData
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SFRConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up SFR box as config entry."""
|
||||
box = SFRBox(ip=entry.data[CONF_HOST], client=async_get_clientsession(hass))
|
||||
platforms = PLATFORMS
|
||||
@@ -33,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SFRConfigEntry) -> bool:
|
||||
raise ConfigEntryNotReady from err
|
||||
platforms = PLATFORMS_WITH_AUTH
|
||||
|
||||
data = SFRRuntimeData(
|
||||
data = DomainData(
|
||||
box=box,
|
||||
dsl=SFRDataUpdateCoordinator(
|
||||
hass, entry, box, "dsl", lambda b: b.dsl_get_info()
|
||||
@@ -62,6 +64,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: SFRConfigEntry) -> bool:
|
||||
tasks.append(data.ftth.async_config_entry_first_refresh())
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
@@ -73,12 +77,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: SFRConfigEntry) -> bool:
|
||||
configuration_url=f"http://{entry.data[CONF_HOST]}",
|
||||
)
|
||||
|
||||
entry.runtime_data = data
|
||||
await hass.config_entries.async_forward_entry_setups(entry, platforms)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SFRConfigEntry) -> 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
|
||||
|
||||
@@ -6,19 +6,23 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sfrbox_api.models import DslInfo, FtthInfo, WanInfo
|
||||
from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import SFRConfigEntry
|
||||
from .entity import SFRCoordinatorEntity
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SFRDataUpdateCoordinator
|
||||
from .models import DomainData
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -59,11 +63,11 @@ WAN_SENSOR_TYPES: tuple[SFRBoxBinarySensorEntityDescription[WanInfo], ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: SFRConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensors."""
|
||||
data = entry.runtime_data
|
||||
data: DomainData = hass.data[DOMAIN][entry.entry_id]
|
||||
system_info = data.system.data
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
@@ -86,10 +90,29 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SFRBoxBinarySensor[_T](SFRCoordinatorEntity[_T], BinarySensorEntity):
|
||||
"""SFR Box binary sensor."""
|
||||
class SFRBoxBinarySensor[_T](
|
||||
CoordinatorEntity[SFRDataUpdateCoordinator[_T]], BinarySensorEntity
|
||||
):
|
||||
"""SFR Box sensor."""
|
||||
|
||||
entity_description: SFRBoxBinarySensorEntityDescription[_T]
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SFRDataUpdateCoordinator[_T],
|
||||
description: SFRBoxBinarySensorEntityDescription,
|
||||
system_info: SystemInfo,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = (
|
||||
f"{system_info.mac_addr}_{coordinator.name}_{description.key}"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, system_info.mac_addr)},
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
|
||||
@@ -16,13 +16,15 @@ from homeassistant.components.button import (
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import SFRConfigEntry
|
||||
from .entity import SFREntity
|
||||
from .const import DOMAIN
|
||||
from .models import DomainData
|
||||
|
||||
|
||||
def with_error_wrapping[**_P, _R](
|
||||
@@ -64,11 +66,11 @@ BUTTON_TYPES: tuple[SFRBoxButtonEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: SFRConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the buttons."""
|
||||
data = entry.runtime_data
|
||||
data: DomainData = hass.data[DOMAIN][entry.entry_id]
|
||||
system_info = data.system.data
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
@@ -79,10 +81,11 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SFRBoxButton(SFREntity, ButtonEntity):
|
||||
"""SFR Box button."""
|
||||
class SFRBoxButton(ButtonEntity):
|
||||
"""Mixin for button specific attributes."""
|
||||
|
||||
entity_description: SFRBoxButtonEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -90,9 +93,13 @@ class SFRBoxButton(SFREntity, ButtonEntity):
|
||||
description: SFRBoxButtonEntityDescription,
|
||||
system_info: SystemInfo,
|
||||
) -> None:
|
||||
"""Initialize the button."""
|
||||
super().__init__(description, system_info)
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
self._box = box
|
||||
self._attr_unique_id = f"{system_info.mac_addr}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, system_info.mac_addr)},
|
||||
)
|
||||
|
||||
@with_error_wrapping
|
||||
async def async_press(self) -> None:
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
"""SFR Box coordinator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from sfrbox_api.bridge import SFRBox
|
||||
from sfrbox_api.exceptions import SFRBoxError
|
||||
from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -19,29 +15,16 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
type SFRConfigEntry = ConfigEntry[SFRRuntimeData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SFRRuntimeData:
|
||||
"""Runtime data for SFR Box."""
|
||||
|
||||
box: SFRBox
|
||||
dsl: SFRDataUpdateCoordinator[DslInfo]
|
||||
ftth: SFRDataUpdateCoordinator[FtthInfo]
|
||||
system: SFRDataUpdateCoordinator[SystemInfo]
|
||||
wan: SFRDataUpdateCoordinator[WanInfo]
|
||||
|
||||
|
||||
class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT | None]):
|
||||
"""Coordinator to manage data updates."""
|
||||
|
||||
config_entry: SFRConfigEntry
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: SFRConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
box: SFRBox,
|
||||
name: str,
|
||||
method: Callable[[SFRBox], Coroutine[Any, Any, _DataT | None]],
|
||||
|
||||
@@ -6,9 +6,11 @@ import dataclasses
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import SFRConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .models import DomainData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import DataclassInstance
|
||||
@@ -23,10 +25,10 @@ def _async_redact_data(obj: DataclassInstance | None) -> dict[str, Any] | None:
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: SFRConfigEntry
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
data = entry.runtime_data
|
||||
data: DomainData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
return {
|
||||
"entry": {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
"""SFR Box base entity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sfrbox_api.models import SystemInfo
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SFRDataUpdateCoordinator
|
||||
|
||||
|
||||
class SFREntity(Entity):
|
||||
"""SFR Box entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, description: EntityDescription, system_info: SystemInfo) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.entity_description = description
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, system_info.mac_addr)},
|
||||
)
|
||||
self._attr_unique_id = f"{system_info.mac_addr}_{description.key}"
|
||||
|
||||
|
||||
class SFRCoordinatorEntity[_T](
|
||||
CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SFREntity
|
||||
):
|
||||
"""SFR Box coordinator entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SFRDataUpdateCoordinator[_T],
|
||||
description: EntityDescription,
|
||||
system_info: SystemInfo,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
SFREntity.__init__(self, description, system_info)
|
||||
self._attr_unique_id = (
|
||||
f"{system_info.mac_addr}_{coordinator.name}_{description.key}"
|
||||
)
|
||||
19
homeassistant/components/sfr_box/models.py
Normal file
19
homeassistant/components/sfr_box/models.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""SFR Box models."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sfrbox_api.bridge import SFRBox
|
||||
from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo
|
||||
|
||||
from .coordinator import SFRDataUpdateCoordinator
|
||||
|
||||
|
||||
@dataclass
|
||||
class DomainData:
|
||||
"""Domain data for SFR Box."""
|
||||
|
||||
box: SFRBox
|
||||
dsl: SFRDataUpdateCoordinator[DslInfo]
|
||||
ftth: SFRDataUpdateCoordinator[FtthInfo]
|
||||
system: SFRDataUpdateCoordinator[SystemInfo]
|
||||
wan: SFRDataUpdateCoordinator[WanInfo]
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
EntityCategory,
|
||||
@@ -20,11 +21,14 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import SFRConfigEntry
|
||||
from .entity import SFRCoordinatorEntity
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SFRDataUpdateCoordinator
|
||||
from .models import DomainData
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -216,11 +220,11 @@ def _get_temperature(value: float | None) -> float | None:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: SFRConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensors."""
|
||||
data = entry.runtime_data
|
||||
data: DomainData = hass.data[DOMAIN][entry.entry_id]
|
||||
system_info = data.system.data
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
@@ -242,10 +246,27 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SFRBoxSensor[_T](SFRCoordinatorEntity[_T], SensorEntity):
|
||||
class SFRBoxSensor[_T](CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SensorEntity):
|
||||
"""SFR Box sensor."""
|
||||
|
||||
entity_description: SFRBoxSensorEntityDescription[_T]
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SFRDataUpdateCoordinator[_T],
|
||||
description: SFRBoxSensorEntityDescription,
|
||||
system_info: SystemInfo,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = (
|
||||
f"{system_info.mac_addr}_{coordinator.name}_{description.key}"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, system_info.mac_addr)},
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "The password for accessing your SFR box's web interface, the default is the WiFi security key found on the device label",
|
||||
"username": "The username for accessing your SFR box's web interface, the default is 'admin'"
|
||||
}
|
||||
},
|
||||
"choose_auth": {
|
||||
|
||||
@@ -417,7 +417,7 @@ def get_rpc_sub_device_name(
|
||||
"""Get name based on device and channel name."""
|
||||
if key in device.config and key != "em:0":
|
||||
# workaround for Pro 3EM, we don't want to get name for em:0
|
||||
if (zone_id := get_irrigation_zone_id(device, key)) is not None:
|
||||
if (zone_id := get_irrigation_zone_id(device.config, key)) is not None:
|
||||
# workaround for Irrigation controller, name stored in "service:0"
|
||||
if zone_name := device.config["service:0"]["zones"][zone_id]["name"]:
|
||||
return cast(str, zone_name)
|
||||
@@ -792,13 +792,9 @@ async def get_rpc_scripts_event_types(
|
||||
return script_events
|
||||
|
||||
|
||||
def get_irrigation_zone_id(device: RpcDevice, key: str) -> int | None:
|
||||
def get_irrigation_zone_id(config: dict[str, Any], key: str) -> int | None:
|
||||
"""Return the zone id if the component is an irrigation zone."""
|
||||
if (
|
||||
device.initialized
|
||||
and key in device.config
|
||||
and (zone := get_rpc_role_by_key(device.config, key)).startswith("zone")
|
||||
):
|
||||
if key in config and (zone := get_rpc_role_by_key(config, key)).startswith("zone"):
|
||||
return int(zone[4:])
|
||||
return None
|
||||
|
||||
@@ -841,7 +837,7 @@ def get_rpc_device_info(
|
||||
if (
|
||||
(
|
||||
component not in (*All_LIGHT_TYPES, "cover", "em1", "switch")
|
||||
and get_irrigation_zone_id(device, key) is None
|
||||
and get_irrigation_zone_id(device.config, key) is None
|
||||
)
|
||||
or idx is None
|
||||
or len(get_rpc_key_instances(device.status, component, all_lights=True)) < 2
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["PySwitchbot==0.72.1"]
|
||||
"requirements": ["PySwitchbot==0.72.0"]
|
||||
}
|
||||
|
||||
@@ -2,21 +2,24 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from aioswitcher.api import SwitcherApi
|
||||
from aioswitcher.api.messages import SwitcherBaseResponse
|
||||
from aioswitcher.api.remotes import SwitcherBreezeRemote
|
||||
from aioswitcher.device import DeviceCategory, DeviceState, ThermostatSwing
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitcherConfigEntry
|
||||
from .const import API_CONTROL_BREEZE_DEVICE, SIGNAL_DEVICE_ADD
|
||||
from .const import SIGNAL_DEVICE_ADD
|
||||
from .coordinator import SwitcherDataUpdateCoordinator
|
||||
from .entity import SwitcherEntity
|
||||
from .utils import get_breeze_remote_manager
|
||||
@@ -28,7 +31,10 @@ PARALLEL_UPDATES = 1
|
||||
class SwitcherThermostatButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Class to describe a Switcher Thermostat Button entity."""
|
||||
|
||||
press_args: dict[str, Any]
|
||||
press_fn: Callable[
|
||||
[SwitcherApi, SwitcherBreezeRemote],
|
||||
Coroutine[Any, Any, SwitcherBaseResponse],
|
||||
]
|
||||
supported: Callable[[SwitcherBreezeRemote], bool]
|
||||
|
||||
|
||||
@@ -37,26 +43,34 @@ THERMOSTAT_BUTTONS = [
|
||||
key="assume_on",
|
||||
translation_key="assume_on",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_args={"state": DeviceState.ON, "update_state": True},
|
||||
press_fn=lambda api, remote: api.control_breeze_device(
|
||||
remote, state=DeviceState.ON, update_state=True
|
||||
),
|
||||
supported=lambda _: True,
|
||||
),
|
||||
SwitcherThermostatButtonEntityDescription(
|
||||
key="assume_off",
|
||||
translation_key="assume_off",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_args={"state": DeviceState.OFF, "update_state": True},
|
||||
press_fn=lambda api, remote: api.control_breeze_device(
|
||||
remote, state=DeviceState.OFF, update_state=True
|
||||
),
|
||||
supported=lambda _: True,
|
||||
),
|
||||
SwitcherThermostatButtonEntityDescription(
|
||||
key="vertical_swing_on",
|
||||
translation_key="vertical_swing_on",
|
||||
press_args={"swing": ThermostatSwing.ON},
|
||||
press_fn=lambda api, remote: api.control_breeze_device(
|
||||
remote, swing=ThermostatSwing.ON
|
||||
),
|
||||
supported=lambda remote: bool(remote.separated_swing_command),
|
||||
),
|
||||
SwitcherThermostatButtonEntityDescription(
|
||||
key="vertical_swing_off",
|
||||
translation_key="vertical_swing_off",
|
||||
press_args={"swing": ThermostatSwing.OFF},
|
||||
press_fn=lambda api, remote: api.control_breeze_device(
|
||||
remote, swing=ThermostatSwing.OFF
|
||||
),
|
||||
supported=lambda remote: bool(remote.separated_swing_command),
|
||||
),
|
||||
]
|
||||
@@ -107,8 +121,23 @@ class SwitcherThermostatButtonEntity(SwitcherEntity, ButtonEntity):
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self._async_call_api(
|
||||
API_CONTROL_BREEZE_DEVICE,
|
||||
self._remote,
|
||||
**self.entity_description.press_args,
|
||||
)
|
||||
response: SwitcherBaseResponse | None = None
|
||||
error = None
|
||||
|
||||
try:
|
||||
async with SwitcherApi(
|
||||
self.coordinator.data.device_type,
|
||||
self.coordinator.data.ip_address,
|
||||
self.coordinator.data.device_id,
|
||||
self.coordinator.data.device_key,
|
||||
) as swapi:
|
||||
response = await self.entity_description.press_fn(swapi, self._remote)
|
||||
except (TimeoutError, OSError, RuntimeError) as err:
|
||||
error = repr(err)
|
||||
|
||||
if error or not response or not response.successful:
|
||||
self.coordinator.last_update_success = False
|
||||
self.async_write_ha_state()
|
||||
raise HomeAssistantError(
|
||||
f"Call api for {self.name} failed, response/error: {response or error}"
|
||||
)
|
||||
|
||||
@@ -27,18 +27,20 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitcherConfigEntry
|
||||
from .const import API_CONTROL_BREEZE_DEVICE, SIGNAL_DEVICE_ADD
|
||||
from .const import SIGNAL_DEVICE_ADD
|
||||
from .coordinator import SwitcherDataUpdateCoordinator
|
||||
from .entity import SwitcherEntity
|
||||
from .utils import get_breeze_remote_manager
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
API_CONTROL_BREEZE_DEVICE = "control_breeze_device"
|
||||
|
||||
DEVICE_MODE_TO_HA = {
|
||||
ThermostatMode.COOL: HVACMode.COOL,
|
||||
ThermostatMode.HEAT: HVACMode.HEAT,
|
||||
@@ -157,16 +159,21 @@ class SwitcherClimateEntity(SwitcherEntity, ClimateEntity):
|
||||
"""Set new target temperature."""
|
||||
data = cast(SwitcherThermostat, self.coordinator.data)
|
||||
if not self._remote.modes_features[data.mode]["temperature_control"]:
|
||||
raise ServiceValidationError(
|
||||
"Current mode does not support setting 'Target temperature'."
|
||||
raise HomeAssistantError(
|
||||
"Current mode doesn't support setting Target Temperature"
|
||||
)
|
||||
|
||||
await self._async_control_breeze_device(
|
||||
target_temp=int(kwargs[ATTR_TEMPERATURE])
|
||||
)
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
raise ValueError("No target temperature provided")
|
||||
|
||||
await self._async_control_breeze_device(target_temp=int(temperature))
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new target fan mode."""
|
||||
data = cast(SwitcherThermostat, self.coordinator.data)
|
||||
if not self._remote.modes_features[data.mode]["fan_levels"]:
|
||||
raise HomeAssistantError("Current mode doesn't support setting Fan Mode")
|
||||
|
||||
await self._async_control_breeze_device(fan_level=HA_TO_DEVICE_FAN[fan_mode])
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
@@ -180,6 +187,10 @@ class SwitcherClimateEntity(SwitcherEntity, ClimateEntity):
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
"""Set new target swing operation."""
|
||||
data = cast(SwitcherThermostat, self.coordinator.data)
|
||||
if not self._remote.modes_features[data.mode]["swing"]:
|
||||
raise HomeAssistantError("Current mode doesn't support setting Swing Mode")
|
||||
|
||||
if swing_mode == SWING_VERTICAL:
|
||||
await self._async_control_breeze_device(swing=ThermostatSwing.ON)
|
||||
else:
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
DOMAIN = "switcher_kis"
|
||||
|
||||
API_CONTROL_BREEZE_DEVICE = "control_breeze_device"
|
||||
|
||||
DISCOVERY_TIME_SEC = 12
|
||||
|
||||
SIGNAL_DEVICE_ADD = "switcher_device_add"
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/switcher_kis",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioswitcher"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioswitcher==6.0.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ rules:
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
docs-removal-instructions: todo
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
@@ -28,7 +28,7 @@ rules:
|
||||
comment: The integration only supports a single config entry.
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
|
||||
@@ -24,8 +24,7 @@ from homeassistant.components.telegram_bot import (
|
||||
)
|
||||
from homeassistant.const import ATTR_LOCATION
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
from homeassistant.helpers.reload import setup_reload_service
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN, PLATFORMS
|
||||
@@ -46,25 +45,14 @@ PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
async def async_get_service(
|
||||
def get_service(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> TelegramNotificationService:
|
||||
"""Get the Telegram notification service."""
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"migrate_notify",
|
||||
breaks_in_ha_version="2026.5.0",
|
||||
is_fixable=False,
|
||||
translation_key="migrate_notify",
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
learn_more_url="https://www.home-assistant.io/integrations/telegram_bot#notifiers",
|
||||
)
|
||||
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
chat_id = config.get(CONF_CHAT_ID)
|
||||
return TelegramNotificationService(hass, chat_id)
|
||||
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
{
|
||||
"issues": {
|
||||
"migrate_notify": {
|
||||
"description": "The Telegram `notify` service has been migrated. A new `notify` entity per chat ID is available now.\n\nUpdate all affected automations to use the new `notify.send_message` action exposed by these new entities and then restart Home Assistant.",
|
||||
"title": "Migration of Telegram notify service"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads telegram notify services.",
|
||||
|
||||
@@ -108,8 +108,8 @@ from .const import (
|
||||
SERVICE_SEND_STICKER,
|
||||
SERVICE_SEND_VIDEO,
|
||||
SERVICE_SEND_VOICE,
|
||||
SIGNAL_UPDATE_EVENT,
|
||||
)
|
||||
from .helpers import signal
|
||||
|
||||
_FILE_TYPES = ("animation", "document", "photo", "sticker", "video", "voice")
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -169,7 +169,7 @@ class BaseTelegramBot:
|
||||
|
||||
_LOGGER.debug("Firing event %s: %s", event_type, event_data)
|
||||
self.hass.bus.async_fire(event_type, event_data, context=event_context)
|
||||
async_dispatcher_send(self.hass, signal(self._bot), event_type, event_data)
|
||||
async_dispatcher_send(self.hass, SIGNAL_UPDATE_EVENT, event_type, event_data)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
@@ -551,7 +551,7 @@ class TelegramNotificationService:
|
||||
EVENT_TELEGRAM_SENT, event_data, context=context
|
||||
)
|
||||
async_dispatcher_send(
|
||||
self.hass, signal(self.bot), EVENT_TELEGRAM_SENT, event_data
|
||||
self.hass, SIGNAL_UPDATE_EVENT, EVENT_TELEGRAM_SENT, event_data
|
||||
)
|
||||
except TelegramError as exc:
|
||||
if not suppress_error:
|
||||
|
||||
@@ -14,9 +14,9 @@ from .const import (
|
||||
EVENT_TELEGRAM_COMMAND,
|
||||
EVENT_TELEGRAM_SENT,
|
||||
EVENT_TELEGRAM_TEXT,
|
||||
SIGNAL_UPDATE_EVENT,
|
||||
)
|
||||
from .entity import TelegramBotEntity
|
||||
from .helpers import signal
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -55,7 +55,7 @@ class TelegramBotEventEntity(TelegramBotEntity, EventEntity):
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
signal(self.config_entry.runtime_data.bot),
|
||||
SIGNAL_UPDATE_EVENT,
|
||||
self._async_handle_event,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
"""Helper functions for Telegram bot integration."""
|
||||
|
||||
from telegram import Bot
|
||||
|
||||
from .const import SIGNAL_UPDATE_EVENT
|
||||
|
||||
|
||||
def signal(bot: Bot) -> str:
|
||||
"""Define signal name."""
|
||||
return f"{SIGNAL_UPDATE_EVENT}_{bot.id}"
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/traccar",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pytraccar"],
|
||||
"requirements": ["pytraccar==3.0.0", "stringcase==1.2.0"]
|
||||
"requirements": ["pytraccar==2.1.1", "stringcase==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ from pytraccar import ApiClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_TOKEN,
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
@@ -19,7 +18,6 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
@@ -35,11 +33,6 @@ PLATFORMS: list[Platform] = [
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Traccar Server from a config entry."""
|
||||
if CONF_API_TOKEN not in entry.data:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="migrate_to_api_token",
|
||||
)
|
||||
client_session = async_create_clientsession(
|
||||
hass,
|
||||
cookie_jar=CookieJar(
|
||||
@@ -53,7 +46,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
client_session=client_session,
|
||||
host=entry.data[CONF_HOST],
|
||||
port=entry.data[CONF_PORT],
|
||||
token=entry.data[CONF_API_TOKEN],
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
ssl=entry.data[CONF_SSL],
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
),
|
||||
@@ -96,15 +90,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle an options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
if entry.version < 2:
|
||||
# Version 2: Remove username and password, only keep API token
|
||||
data = dict(entry.data)
|
||||
data.pop(CONF_USERNAME, None)
|
||||
data.pop(CONF_PASSWORD, None)
|
||||
hass.config_entries.async_update_entry(entry, data=data, version=2)
|
||||
return True
|
||||
|
||||
@@ -16,10 +16,11 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_API_TOKEN,
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
@@ -60,7 +61,10 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_PORT, default="8082"): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.TEXT)
|
||||
),
|
||||
vol.Required(CONF_API_TOKEN): TextSelector(
|
||||
vol.Required(CONF_USERNAME): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.EMAIL)
|
||||
),
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
||||
),
|
||||
vol.Optional(CONF_SSL, default=False): BooleanSelector(BooleanSelectorConfig()),
|
||||
@@ -116,17 +120,16 @@ OPTIONS_FLOW = {
|
||||
class TraccarServerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Traccar Server."""
|
||||
|
||||
VERSION = 2
|
||||
|
||||
async def _get_server_info(self, user_input: dict[str, Any]) -> ServerModel:
|
||||
"""Get server info."""
|
||||
client = ApiClient(
|
||||
client_session=async_get_clientsession(self.hass),
|
||||
host=user_input[CONF_HOST],
|
||||
port=user_input[CONF_PORT],
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
ssl=user_input[CONF_SSL],
|
||||
verify_ssl=user_input[CONF_VERIFY_SSL],
|
||||
token=user_input[CONF_API_TOKEN],
|
||||
)
|
||||
return await client.get_server()
|
||||
|
||||
@@ -198,11 +201,19 @@ class TraccarServerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
reauth_entry,
|
||||
data_updates=user_input,
|
||||
)
|
||||
username = (
|
||||
user_input[CONF_USERNAME]
|
||||
if user_input
|
||||
else reauth_entry.data[CONF_USERNAME]
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_TOKEN): TextSelector(
|
||||
vol.Required(CONF_USERNAME, default=username): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.EMAIL)
|
||||
),
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
||||
),
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/traccar_server",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["pytraccar==3.0.0"]
|
||||
"requirements": ["pytraccar==2.1.1"]
|
||||
}
|
||||
|
||||
@@ -12,21 +12,23 @@
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"description": "The authentication credentials for {host}:{port} need to be updated."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]",
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_token": "The API token generated from your account on your Traccar Server",
|
||||
"host": "The hostname or IP address of your Traccar Server"
|
||||
"host": "The hostname or IP address of your Traccar Server",
|
||||
"username": "The username (email) you use to log in to your Traccar Server"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,11 +62,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"migrate_to_api_token": {
|
||||
"message": "To continue using Traccar Server, you need to migrate to API token based authentication."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
@@ -222,24 +222,6 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
|
||||
on_value={"AQAB"},
|
||||
),
|
||||
),
|
||||
DeviceCategory.MSP: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=f"{DPCode.FAULT}_full_fault",
|
||||
dpcode=DPCode.FAULT,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
bitmap_key="full_fault",
|
||||
translation_key="bag_full",
|
||||
),
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=f"{DPCode.FAULT}_box_out",
|
||||
dpcode=DPCode.FAULT,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
bitmap_key="box_out",
|
||||
translation_key="cover_off",
|
||||
),
|
||||
),
|
||||
DeviceCategory.PIR: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.PIR,
|
||||
|
||||
@@ -714,8 +714,6 @@ class DPCode(StrEnum):
|
||||
ECO2 = "eco2"
|
||||
EDGE_BRUSH = "edge_brush"
|
||||
ELECTRICITY_LEFT = "electricity_left"
|
||||
EXCRETION_TIME_DAY = "excretion_time_day"
|
||||
EXCRETION_TIMES_DAY = "excretion_times_day"
|
||||
FAN_BEEP = "fan_beep" # Sound
|
||||
FAN_COOL = "fan_cool" # Cool wind
|
||||
FAN_DIRECTION = "fan_direction" # Fan direction
|
||||
|
||||
@@ -760,15 +760,6 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.WEIGHT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.EXCRETION_TIME_DAY,
|
||||
translation_key="excretion_time_day",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.EXCRETION_TIMES_DAY,
|
||||
translation_key="excretion_times_day",
|
||||
),
|
||||
),
|
||||
DeviceCategory.MZJ: (
|
||||
TuyaSensorEntityDescription(
|
||||
|
||||
@@ -27,18 +27,12 @@
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"bag_full": {
|
||||
"name": "Bag full"
|
||||
},
|
||||
"carbon_dioxide": {
|
||||
"name": "Carbon dioxide"
|
||||
},
|
||||
"carbon_monoxide": {
|
||||
"name": "Carbon monoxide"
|
||||
},
|
||||
"cover_off": {
|
||||
"name": "Cover off"
|
||||
},
|
||||
"defrost": {
|
||||
"name": "Defrost"
|
||||
},
|
||||
@@ -626,12 +620,6 @@
|
||||
"duster_cloth_life": {
|
||||
"name": "Duster cloth lifetime"
|
||||
},
|
||||
"excretion_time_day": {
|
||||
"name": "Excretion time (day)"
|
||||
},
|
||||
"excretion_times_day": {
|
||||
"name": "Excretion times (day)"
|
||||
},
|
||||
"feels_like_temperature": {
|
||||
"name": "Feels like"
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user