Compare commits

..

4 Commits

Author SHA1 Message Date
Ludovic BOUÉ
08eee8d479 Update tests/components/matter/test_switch.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-29 11:40:54 +01:00
Ludovic BOUÉ
6bbaae7235 Update homeassistant/components/matter/switch.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-29 11:40:39 +01:00
Ludovic BOUÉ
86a5dff3f5 Add test for MatterError handling in numeric switch with Eve Thermo fixture 2025-10-29 10:15:35 +00:00
Ludovic BOUÉ
34e137005d Add error handling for Matter switch commands 2025-10-29 10:08:43 +00:00
180 changed files with 976 additions and 4436 deletions

2
Dockerfile generated
View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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:

View File

@@ -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"

View File

@@ -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"]
}

View File

@@ -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),

View File

@@ -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}"

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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"]
}

View File

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

View File

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

View File

@@ -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." }
]

View File

@@ -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:

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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__)

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

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

View File

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

View File

@@ -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,
)

View File

@@ -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:

View File

@@ -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"
]
}

View File

@@ -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()

View File

@@ -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."""

View File

@@ -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(

View File

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

View File

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

View File

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

View File

@@ -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,
)

View File

@@ -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:

View File

@@ -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(

View File

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

View File

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

View File

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

View File

@@ -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,
)

View File

@@ -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:

View File

@@ -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]}/",
)

View File

@@ -299,8 +299,8 @@ def _create_climate_ui(xknx: XKNX, conf: ConfigExtractor, name: str) -> XknxClim
group_address_active_state=conf.get_state_and_passive(CONF_GA_ACTIVE),
group_address_command_value_state=conf.get_state_and_passive(CONF_GA_VALVE),
sync_state=sync_state,
min_temp=conf.get(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

View File

@@ -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
}

View File

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

View File

@@ -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(",", ".")

View File

@@ -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)

View File

@@ -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

View File

@@ -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]})

View File

@@ -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%]"

View File

@@ -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},

View File

@@ -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"

View File

@@ -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

View File

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

View File

@@ -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": {

View File

@@ -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:

View File

@@ -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"]
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,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]

View File

@@ -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

View File

@@ -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."""

View File

@@ -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()

View File

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

View File

@@ -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"]
}

View File

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

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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]],

View File

@@ -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": {

View File

@@ -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}"
)

View 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]

View File

@@ -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:

View File

@@ -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": {

View File

@@ -417,7 +417,7 @@ def get_rpc_sub_device_name(
"""Get name based on device and channel name."""
if key in device.config and key != "em:0":
# workaround for Pro 3EM, we don't want to get name for em:0
if (zone_id := get_irrigation_zone_id(device, 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

View File

@@ -41,5 +41,5 @@
"iot_class": "local_push",
"loggers": ["switchbot"],
"quality_scale": "gold",
"requirements": ["PySwitchbot==0.72.1"]
"requirements": ["PySwitchbot==0.72.0"]
}

View File

@@ -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}"
)

View File

@@ -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:

View File

@@ -2,8 +2,6 @@
DOMAIN = "switcher_kis"
API_CONTROL_BREEZE_DEVICE = "control_breeze_device"
DISCOVERY_TIME_SEC = 12
SIGNAL_DEVICE_ADD = "switcher_device_add"

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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.",

View File

@@ -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:

View File

@@ -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,
)
)

View File

@@ -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}"

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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)
),
}

View File

@@ -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"]
}

View File

@@ -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": {

View File

@@ -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,

View File

@@ -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

View File

@@ -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(

View File

@@ -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