From 307c6a4ce26efad558c46593d9cda53b36cb9b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Farkasdi?= <93778865+farkasdi@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:34:23 +0100 Subject: [PATCH 01/36] Netatmo doortag binary sensor addition (#160608) --- .../components/netatmo/binary_sensor.py | 340 ++++++++++++++++-- homeassistant/components/netatmo/const.py | 19 + .../components/netatmo/data_handler.py | 6 + homeassistant/components/netatmo/entity.py | 14 +- homeassistant/components/netatmo/strings.json | 11 + tests/components/netatmo/common.py | 4 + .../netatmo/snapshots/test_binary_sensor.ambr | 102 ++++++ .../components/netatmo/test_binary_sensor.py | 325 ++++++++++++++++- 8 files changed, 786 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 81498c3d76707b..c550c31c4a6c90 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -1,8 +1,12 @@ """Support for Netatmo binary sensors.""" +from collections.abc import Callable from dataclasses import dataclass +from functools import partial import logging -from typing import Final, cast +from typing import Any, Final, cast + +from pyatmo.modules.device_types import DeviceCategory as NetatmoDeviceCategory from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -13,34 +17,166 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.typing import StateType -from .const import NETATMO_CREATE_WEATHER_BINARY_SENSOR -from .data_handler import NetatmoDevice -from .entity import NetatmoWeatherModuleEntity +from .const import ( + CONF_URL_SECURITY, + DOORTAG_CATEGORY_DOOR, + DOORTAG_CATEGORY_FURNITURE, + DOORTAG_CATEGORY_GARAGE, + DOORTAG_CATEGORY_GATE, + DOORTAG_CATEGORY_OTHER, + DOORTAG_CATEGORY_WINDOW, + DOORTAG_STATUS_CALIBRATING, + DOORTAG_STATUS_CALIBRATION_FAILED, + DOORTAG_STATUS_CLOSED, + DOORTAG_STATUS_MAINTENANCE, + DOORTAG_STATUS_NO_NEWS, + DOORTAG_STATUS_OPEN, + DOORTAG_STATUS_UNDEFINED, + DOORTAG_STATUS_WEAK_SIGNAL, + NETATMO_CREATE_CONNECTIVITY_BINARY_SENSOR, + NETATMO_CREATE_OPENING_BINARY_SENSOR, + NETATMO_CREATE_WEATHER_BINARY_SENSOR, +) +from .data_handler import SIGNAL_NAME, NetatmoDevice +from .entity import NetatmoModuleEntity, NetatmoWeatherModuleEntity _LOGGER = logging.getLogger(__name__) +DEFAULT_OPENING_SENSOR_KEY = "opening_sensor" + +OPENING_STATUS_TO_BINARY_SENSOR_STATE: Final[dict[str, bool | None]] = { + DOORTAG_STATUS_NO_NEWS: None, + DOORTAG_STATUS_CALIBRATING: None, + DOORTAG_STATUS_UNDEFINED: None, + DOORTAG_STATUS_CLOSED: False, + DOORTAG_STATUS_OPEN: True, + DOORTAG_STATUS_CALIBRATION_FAILED: None, + DOORTAG_STATUS_MAINTENANCE: None, + DOORTAG_STATUS_WEAK_SIGNAL: None, +} + + +OPENING_CATEGORY_TO_DEVICE_CLASS: Final[dict[str | None, BinarySensorDeviceClass]] = { + DOORTAG_CATEGORY_DOOR: BinarySensorDeviceClass.DOOR, + DOORTAG_CATEGORY_FURNITURE: BinarySensorDeviceClass.OPENING, + DOORTAG_CATEGORY_GARAGE: BinarySensorDeviceClass.GARAGE_DOOR, + DOORTAG_CATEGORY_GATE: BinarySensorDeviceClass.OPENING, + DOORTAG_CATEGORY_OTHER: BinarySensorDeviceClass.OPENING, + DOORTAG_CATEGORY_WINDOW: BinarySensorDeviceClass.WINDOW, +} + + +def get_opening_category(netatmo_device: NetatmoDevice) -> str: + """Helper function to get opening category from Netatmo API raw data.""" + + # Iterate through each home in the raw data. + for home in netatmo_device.data_handler.account.raw_data["homes"]: + # Check if the modules list exists for the current home. + if "modules" in home: + # Iterate through each module to find a matching ID. + for module in home["modules"]: + if module["id"] == netatmo_device.device.entity_id: + # We found the matching device. Get its category. + if module.get("category") is not None: + return cast(str, module["category"]) + raise ValueError( + f"Device {netatmo_device.device.entity_id} found, " + "but 'category' is missing in raw data." + ) + + raise ValueError( + f"Device {netatmo_device.device.entity_id} not found in Netatmo raw data." + ) + + +OPENING_CATEGORY_TO_KEY: Final[dict[str, str | None]] = { + DOORTAG_CATEGORY_DOOR: None, + DOORTAG_CATEGORY_FURNITURE: DOORTAG_CATEGORY_FURNITURE, + DOORTAG_CATEGORY_GARAGE: None, + DOORTAG_CATEGORY_GATE: DOORTAG_CATEGORY_GATE, + DOORTAG_CATEGORY_OTHER: DEFAULT_OPENING_SENSOR_KEY, + DOORTAG_CATEGORY_WINDOW: None, +} + @dataclass(frozen=True, kw_only=True) class NetatmoBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes Netatmo binary sensor entity.""" - name: str | None = None # The default name of the sensor - netatmo_name: str # The name used by Netatmo API for this sensor + netatmo_name: str | None = ( + None # The name used by Netatmo API for this sensor (exposed feature as attribute) if different than key + ) + value_fn: Callable[[str], str | bool | None] = lambda x: x -NETATMO_WEATHER_BINARY_SENSOR_DESCRIPTIONS: Final[ +NETATMO_CONNECTIVITY_BINARY_SENSOR_DESCRIPTIONS: Final[ list[NetatmoBinarySensorEntityDescription] ] = [ NetatmoBinarySensorEntityDescription( key="reachable", - name="Connectivity", - netatmo_name="reachable", device_class=BinarySensorDeviceClass.CONNECTIVITY, ), ] +# Assuming a Module object with the following attributes: +# {'battery_level': 5780, +# 'battery_percent': None, +# 'battery_state': 'full', +# 'bridge': 'XX:XX:XX:XX:XX:XX', +# 'device_category': , +# 'device_type': , +# 'entity_id': 'NN:NN:NN:NN:NN:NN', +# 'features': {'status', 'battery', 'rf_strength', 'reachable'}, +# 'firmware_name': None, +# 'firmware_revision': 58, +# 'history_features': set(), +# 'history_features_values': {}, +# 'home': , +# 'modules': None, +# 'name': 'YYYYYY', +# 'reachable': True, +# 'rf_strength': 74, +# 'room_id': 'ZZZZZZZZ', +# 'status': 'open'} + +NETATMO_OPENING_BINARY_SENSOR_DESCRIPTIONS: Final[ + list[NetatmoBinarySensorEntityDescription] +] = [ + NetatmoBinarySensorEntityDescription( + key="opening", + netatmo_name="status", + value_fn=OPENING_STATUS_TO_BINARY_SENSOR_STATE.get, + ), +] + +DEVICE_CATEGORY_BINARY_URLS: Final[dict[NetatmoDeviceCategory, str]] = { + NetatmoDeviceCategory.opening: CONF_URL_SECURITY, +} + +DEVICE_CATEGORY_WEATHER_BINARY_SENSORS: Final[ + dict[NetatmoDeviceCategory, list[NetatmoBinarySensorEntityDescription]] +] = { + NetatmoDeviceCategory.air_care: NETATMO_CONNECTIVITY_BINARY_SENSOR_DESCRIPTIONS, + NetatmoDeviceCategory.weather: NETATMO_CONNECTIVITY_BINARY_SENSOR_DESCRIPTIONS, +} + +DEVICE_CATEGORY_CONNECTIVITY_BINARY_SENSORS: Final[ + dict[NetatmoDeviceCategory, list[NetatmoBinarySensorEntityDescription]] +] = { + NetatmoDeviceCategory.opening: NETATMO_CONNECTIVITY_BINARY_SENSOR_DESCRIPTIONS, +} + +DEVICE_CATEGORY_OPENING_BINARY_SENSORS: Final[ + dict[NetatmoDeviceCategory, list[NetatmoBinarySensorEntityDescription]] +] = { + NetatmoDeviceCategory.opening: NETATMO_OPENING_BINARY_SENSOR_DESCRIPTIONS, +} + +DEVICE_CATEGORY_BINARY_PUBLISHERS: Final[list[NetatmoDeviceCategory]] = [ + NetatmoDeviceCategory.opening, +] + async def async_setup_entry( hass: HomeAssistant, @@ -50,25 +186,47 @@ async def async_setup_entry( """Set up Netatmo weather binary sensors based on a config entry.""" @callback - def _create_weather_binary_sensor_entity(netatmo_device: NetatmoDevice) -> None: - """Create weather binary sensor entities for a Netatmo weather device.""" + def _create_binary_sensor_entity( + binarySensorClass: type[ + NetatmoWeatherBinarySensor + | NetatmoOpeningBinarySensor + | NetatmoConnectivityBinarySensor + ], + descriptions: dict[ + NetatmoDeviceCategory, list[NetatmoBinarySensorEntityDescription] + ], + netatmo_device: NetatmoDevice, + ) -> None: + """Create binary sensor entities for a Netatmo device.""" - descriptions_to_add = NETATMO_WEATHER_BINARY_SENSOR_DESCRIPTIONS + if netatmo_device.device.device_category is None: + return - entities: list[NetatmoWeatherBinarySensor] = [] + descriptions_to_add = descriptions.get( + netatmo_device.device.device_category, [] + ) + + entities: list[ + NetatmoWeatherBinarySensor + | NetatmoOpeningBinarySensor + | NetatmoConnectivityBinarySensor + ] = [] # Create binary sensors for module for description in descriptions_to_add: - # Actual check is simple for reachable - feature_check = description.key + if description.netatmo_name is None: + feature_check = description.key + else: + feature_check = description.netatmo_name if feature_check in netatmo_device.device.features: _LOGGER.debug( - 'Adding "%s" weather binary sensor for device %s', + 'Adding "%s" (native: "%s") binary sensor for device %s', + description.key, feature_check, netatmo_device.device.name, ) entities.append( - NetatmoWeatherBinarySensor( + binarySensorClass( netatmo_device, description, ) @@ -81,35 +239,89 @@ def _create_weather_binary_sensor_entity(netatmo_device: NetatmoDevice) -> None: async_dispatcher_connect( hass, NETATMO_CREATE_WEATHER_BINARY_SENSOR, - _create_weather_binary_sensor_entity, + partial( + _create_binary_sensor_entity, + NetatmoWeatherBinarySensor, + DEVICE_CATEGORY_WEATHER_BINARY_SENSORS, + ), ) ) + entry.async_on_unload( + async_dispatcher_connect( + hass, + NETATMO_CREATE_OPENING_BINARY_SENSOR, + partial( + _create_binary_sensor_entity, + NetatmoOpeningBinarySensor, + DEVICE_CATEGORY_OPENING_BINARY_SENSORS, + ), + ) + ) -class NetatmoWeatherBinarySensor(NetatmoWeatherModuleEntity, BinarySensorEntity): - """Implementation of a Netatmo weather binary sensor.""" + entry.async_on_unload( + async_dispatcher_connect( + hass, + NETATMO_CREATE_CONNECTIVITY_BINARY_SENSOR, + partial( + _create_binary_sensor_entity, + NetatmoConnectivityBinarySensor, + DEVICE_CATEGORY_CONNECTIVITY_BINARY_SENSORS, + ), + ) + ) + + +class NetatmoBinarySensor(NetatmoModuleEntity, BinarySensorEntity): + """Implementation of a Netatmo binary sensor.""" entity_description: NetatmoBinarySensorEntityDescription + _attr_has_entity_name = True def __init__( self, netatmo_device: NetatmoDevice, description: NetatmoBinarySensorEntityDescription, + **kwargs: Any, # Add this to capture extra args from super() ) -> None: - """Initialize a Netatmo weather binary sensor.""" + """Initialize a Netatmo binary sensor.""" + + # To prevent exception about missing URL we need to set it explicitly + if netatmo_device.device.device_category is not None: + if ( + DEVICE_CATEGORY_BINARY_URLS.get(netatmo_device.device.device_category) + is not None + ): + self._attr_configuration_url = DEVICE_CATEGORY_BINARY_URLS[ + netatmo_device.device.device_category + ] - super().__init__(netatmo_device) + super().__init__(netatmo_device, **kwargs) self.entity_description = description self._attr_unique_id = f"{self.device.entity_id}-{description.key}" + # Register publishers for the entity if needed (not already done in parent class - weather and air_care) + # We need to keep this here because we have two classes depending on it and we want to avoid adding publishers for all binary sensors + if self.device.device_category in DEVICE_CATEGORY_BINARY_PUBLISHERS: + self._publishers.extend( + [ + { + "name": self.home.entity_id, + "home_id": self.home.entity_id, + SIGNAL_NAME: netatmo_device.signal_name, + }, + ] + ) + @callback def async_update_callback(self) -> None: """Update the entity's state.""" - value: StateType | None = None + # Should be the connectivity (reachable) sensor only here as we have update for opening in its class - value = getattr(self.device, self.entity_description.netatmo_name, None) + # Setting reachable sensor, so we just get it directly (backward compatibility to weather binary sensor) + value = getattr(self.device, self.entity_description.key, None) if value is None: self._attr_available = False @@ -117,5 +329,83 @@ def async_update_callback(self) -> None: else: self._attr_available = True self._attr_is_on = cast(bool, value) + self.async_write_ha_state() + + +class NetatmoWeatherBinarySensor(NetatmoWeatherModuleEntity, NetatmoBinarySensor): + """Implementation of a Netatmo weather binary sensor.""" + + entity_description: NetatmoBinarySensorEntityDescription + + def __init__( + self, + netatmo_device: NetatmoDevice, + description: NetatmoBinarySensorEntityDescription, + ) -> None: + """Initialize a Netatmo weather binary sensor.""" + + super().__init__(netatmo_device, description=description) + + +class NetatmoOpeningBinarySensor(NetatmoBinarySensor): + """Implementation of a Netatmo opening binary sensor.""" + + entity_description: NetatmoBinarySensorEntityDescription + _attr_has_entity_name = True + + def __init__( + self, + netatmo_device: NetatmoDevice, + description: NetatmoBinarySensorEntityDescription, + ) -> None: + """Initialize a Netatmo binary sensor.""" + + super().__init__(netatmo_device, description) + + # Apply Dynamic Device Class override + self._attr_device_class = OPENING_CATEGORY_TO_DEVICE_CLASS.get( + get_opening_category(netatmo_device), BinarySensorDeviceClass.OPENING + ) + + # Apply Dynamic Translation Key override if needed + translation_key = OPENING_CATEGORY_TO_KEY.get( + get_opening_category(netatmo_device), DEFAULT_OPENING_SENSOR_KEY + ) + if translation_key is not None: + self._attr_translation_key = translation_key + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + + if not self.device.reachable: + # If reachable is None or False we set availability to False + self._attr_available = False + self._attr_is_on = None + + else: + # If reachable is True, we get the actual value + if self.entity_description.netatmo_name is None: + raw_value = getattr(self.device, self.entity_description.key, None) + else: + raw_value = getattr( + self.device, self.entity_description.netatmo_name, None + ) + + if raw_value is not None: + value = self.entity_description.value_fn(raw_value) + else: + value = None + + # Set sensor state + self._attr_available = True + self._attr_is_on = cast(bool, value) if value is not None else None self.async_write_ha_state() + + +class NetatmoConnectivityBinarySensor(NetatmoBinarySensor): + """Implementation of a Netatmo connectivity binary sensor.""" + + entity_description: NetatmoBinarySensorEntityDescription + _attr_has_entity_name = True diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index e789885f56b437..9a95cd36fed3e8 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -46,9 +46,11 @@ NETATMO_CREATE_CAMERA_LIGHT = "netatmo_create_camera_light" NETATMO_CREATE_CLIMATE = "netatmo_create_climate" NETATMO_CREATE_COVER = "netatmo_create_cover" +NETATMO_CREATE_CONNECTIVITY_BINARY_SENSOR = "netatmo_create_connectivity_binary_sensor" NETATMO_CREATE_BUTTON = "netatmo_create_button" NETATMO_CREATE_FAN = "netatmo_create_fan" NETATMO_CREATE_LIGHT = "netatmo_create_light" +NETATMO_CREATE_OPENING_BINARY_SENSOR = "netatmo_create_opening_binary_sensor" NETATMO_CREATE_ROOM_SENSOR = "netatmo_create_room_sensor" NETATMO_CREATE_SELECT = "netatmo_create_select" NETATMO_CREATE_SENSOR = "netatmo_create_sensor" @@ -191,6 +193,23 @@ MODE_LIGHT_ON = "on" CAMERA_LIGHT_MODES = [MODE_LIGHT_ON, MODE_LIGHT_OFF, MODE_LIGHT_AUTO] +# Door tag categories +DOORTAG_CATEGORY_DOOR = "door" +DOORTAG_CATEGORY_FURNITURE = "furniture" +DOORTAG_CATEGORY_GARAGE = "garage" +DOORTAG_CATEGORY_GATE = "gate" +DOORTAG_CATEGORY_OTHER = "other" +DOORTAG_CATEGORY_WINDOW = "window" +# Door tag statuses +DOORTAG_STATUS_CALIBRATING = "calibrating" +DOORTAG_STATUS_CALIBRATION_FAILED = "calibration_failed" +DOORTAG_STATUS_CLOSED = "closed" +DOORTAG_STATUS_MAINTENANCE = "maintenance" +DOORTAG_STATUS_NO_NEWS = "no_news" +DOORTAG_STATUS_OPEN = "open" +DOORTAG_STATUS_UNDEFINED = "undefined" +DOORTAG_STATUS_WEAK_SIGNAL = "weak_signal" + # Webhook push_types MUST follow exactly Netatmo's naming on products! # See https://dev.netatmo.com/apidocumentation # e.g. cameras: NACamera, NOC, etc. diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index ee1b369c58c987..31845e1c0c7c42 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -38,9 +38,11 @@ NETATMO_CREATE_CAMERA, NETATMO_CREATE_CAMERA_LIGHT, NETATMO_CREATE_CLIMATE, + NETATMO_CREATE_CONNECTIVITY_BINARY_SENSOR, NETATMO_CREATE_COVER, NETATMO_CREATE_FAN, NETATMO_CREATE_LIGHT, + NETATMO_CREATE_OPENING_BINARY_SENSOR, NETATMO_CREATE_ROOM_SENSOR, NETATMO_CREATE_SELECT, NETATMO_CREATE_SENSOR, @@ -367,6 +369,10 @@ def setup_modules(self, home: pyatmo.Home, signal_home: str) -> None: ], NetatmoDeviceCategory.meter: [NETATMO_CREATE_SENSOR], NetatmoDeviceCategory.fan: [NETATMO_CREATE_FAN], + NetatmoDeviceCategory.opening: [ + NETATMO_CREATE_CONNECTIVITY_BINARY_SENSOR, + NETATMO_CREATE_OPENING_BINARY_SENSOR, + ], } for module in home.modules.values(): if not module.device_category: diff --git a/homeassistant/components/netatmo/entity.py b/homeassistant/components/netatmo/entity.py index b519c75ae554ac..2d12631a3db0f9 100644 --- a/homeassistant/components/netatmo/entity.py +++ b/homeassistant/components/netatmo/entity.py @@ -93,9 +93,11 @@ def async_update_callback(self) -> None: class NetatmoDeviceEntity(NetatmoBaseEntity): """Netatmo entity base class.""" - def __init__(self, data_handler: NetatmoDataHandler, device: NetatmoBase) -> None: + def __init__( + self, data_handler: NetatmoDataHandler, device: NetatmoBase, **kwargs: Any + ) -> None: """Set up Netatmo entity base.""" - super().__init__(data_handler) + super().__init__(data_handler, **kwargs) self.device = device @property @@ -153,9 +155,9 @@ class NetatmoModuleEntity(NetatmoDeviceEntity): device: Module _attr_configuration_url: str - def __init__(self, device: NetatmoDevice) -> None: + def __init__(self, device: NetatmoDevice, **kwargs: Any) -> None: """Set up a Netatmo module entity.""" - super().__init__(device.data_handler, device.device) + super().__init__(device.data_handler, device.device, **kwargs) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device.device.entity_id)}, name=device.device.name, @@ -175,9 +177,9 @@ class NetatmoWeatherModuleEntity(NetatmoModuleEntity): _attr_configuration_url = CONF_URL_WEATHER - def __init__(self, device: NetatmoDevice) -> None: + def __init__(self, device: NetatmoDevice, **kwargs: Any) -> None: """Set up a Netatmo weather module entity.""" - super().__init__(device) + super().__init__(device, **kwargs) assert self.device.device_category category = self.device.device_category.name self._publishers.extend( diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index 3307dcb6d4592e..0aadcbfea13f28 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -54,6 +54,17 @@ } }, "entity": { + "binary_sensor": { + "furniture": { + "name": "Furniture" + }, + "gate": { + "name": "Gate" + }, + "opening_sensor": { + "name": "Opening" + } + }, "button": { "preferred_position": { "name": "Preferred position" diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index 617a0070f5c468..90c3bcb55d188a 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -83,6 +83,10 @@ async def fake_post_request(hass: HomeAssistant, *args: Any, **kwargs: Any): else: payload = json.loads(await async_load_fixture(hass, f"{endpoint}.json", DOMAIN)) + # Apply test-specific modifications to the payload + if "msg_callback" in kwargs: + kwargs["msg_callback"](payload) + return AiohttpClientMockResponse( method="POST", url=kwargs["endpoint"], diff --git a/tests/components/netatmo/snapshots/test_binary_sensor.ambr b/tests/components/netatmo/snapshots/test_binary_sensor.ambr index fe50b59f183b80..9558edc0431242 100644 --- a/tests/components/netatmo/snapshots/test_binary_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_binary_sensor.ambr @@ -572,3 +572,105 @@ 'state': 'on', }) # --- +# name: test_entity[binary_sensor.window_hall_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.window_hall_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Connectivity', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:00:86:99-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.window_hall_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Window Hall Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.window_hall_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity[binary_sensor.window_hall_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.window_hall_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Window', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:00:86:99-opening', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.window_hall_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'window', + 'friendly_name': 'Window Hall Window', + }), + 'context': , + 'entity_id': 'binary_sensor.window_hall_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/netatmo/test_binary_sensor.py b/tests/components/netatmo/test_binary_sensor.py index 91d2b3ad63beef..f9dc4aaadcbf51 100644 --- a/tests/components/netatmo/test_binary_sensor.py +++ b/tests/components/netatmo/test_binary_sensor.py @@ -1,17 +1,27 @@ """Support for Netatmo binary sensors.""" -from unittest.mock import AsyncMock +from datetime import timedelta +from typing import Any +from unittest.mock import AsyncMock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.const import CONF_WEBHOOK_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util -from .common import snapshot_platform_entities +from .common import ( + FAKE_WEBHOOK_ACTIVATION, + fake_post_request, + simulate_webhook, + snapshot_platform_entities, +) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -30,3 +40,310 @@ async def test_entity( entity_registry, snapshot, ) + + +async def test_doortag_setup( + hass: HomeAssistant, config_entry: MockConfigEntry, netatmo_auth: AsyncMock +) -> None: + """Test doortag setup.""" + fake_post_hits = 0 + + async def fake_post(*args: Any, **kwargs: Any): + """Fake error during requesting backend data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + return await fake_post_request(hass, *args, **kwargs) + + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, + patch( + "homeassistant.components.netatmo.data_handler.PLATFORMS", + ["camera", "binary_sensor"], + ), + patch( + "homeassistant.components.netatmo.async_get_config_entry_implementation", + return_value=AsyncMock(), + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url", + ) as mock_webhook, + ): + mock_auth.return_value.async_post_api_request.side_effect = fake_post + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + mock_webhook.return_value = "https://example.com" + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + + # Fake webhook activation + await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) + await hass.async_block_till_done() + + # Define variables for the test + _doortag_entity = "window_hall" + _doortag_entity_opening = f"binary_sensor.{_doortag_entity}_window" + _doortag_entity_connectivity = f"binary_sensor.{_doortag_entity}_connectivity" + + # Check opening creation + assert hass.states.get(_doortag_entity_opening) is not None + # Check connectivity creation + assert hass.states.get(_doortag_entity_connectivity) is not None + + # Check opening initial state + assert hass.states.get(_doortag_entity_opening).state == "unavailable" + # Check connectivity initial state + assert hass.states.get(_doortag_entity_connectivity).state == "off" + + +@pytest.mark.parametrize( + ("doortag_status", "expected"), + [ + ("no_news", "unknown"), + ("calibrating", "unknown"), + ("undefined", "unknown"), + ("closed", "off"), + ("open", "on"), + ("calibration_failed", "unknown"), + ("maintenance", "unknown"), + ("weak_signal", "unknown"), + ("invalid_value", "unknown"), + ], +) +async def test_doortag_opening_status_change( + hass: HomeAssistant, + config_entry: MockConfigEntry, + netatmo_auth: AsyncMock, + freezer: FrozenDateTimeFactory, + doortag_status: str, + expected: str, +) -> None: + """Test doortag opening status changes.""" + fake_post_hits = 0 + # Repeatedly used variables for the test and initial value from fixture + # Use nonexistent ID to prevent matching during initial setup + doortag_entity_id = "aa:bb:cc:dd:ee:ff" + doortag_connectivity = False + doortag_opening = "no_news" + doortag_timestamp = None + + def tag_modifier(payload): + """This function will be called by common.py during ANY homestatus call.""" + nonlocal doortag_connectivity, doortag_opening, doortag_timestamp + + if doortag_timestamp is not None: + payload["time_server"] = doortag_timestamp + body = payload.get("body", {}) + + # Handle both structures: {"home": {...}} AND {"homes": [{...}]} + homes_to_check = [] + if "home" in body and isinstance(body["home"], dict): + homes_to_check.append(body["home"]) + elif "homes" in body and isinstance(body["homes"], list): + homes_to_check.extend(body["homes"]) + + for home_data in homes_to_check: + # Safety check: ensure home_data is actually a dictionary + if not isinstance(home_data, dict): + continue + + modules = home_data.get("modules", []) + for module in modules: + if isinstance(module, dict) and module.get("id") == doortag_entity_id: + module["reachable"] = doortag_connectivity + module["status"] = doortag_opening + if doortag_timestamp is not None: + module["last_seen"] = doortag_timestamp + break + + async def fake_tag_post(*args, **kwargs): + """Fake tag status during requesting backend data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + return await fake_post_request(hass, *args, msg_callback=tag_modifier, **kwargs) + + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, + patch( + "homeassistant.components.netatmo.data_handler.PLATFORMS", + ["camera", "binary_sensor"], + ), + patch( + "homeassistant.components.netatmo.async_get_config_entry_implementation", + return_value=AsyncMock(), + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url", + ) as mock_webhook, + ): + mock_auth.return_value.async_post_api_request.side_effect = fake_tag_post + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + mock_webhook.return_value = "https://example.com" + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + + # Fake webhook activation + await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) + await hass.async_block_till_done() + + # Define the variables for the test + _doortag_entity = "window_hall" + _doortag_entity_opening = f"binary_sensor.{_doortag_entity}_window" + _doortag_entity_connectivity = f"binary_sensor.{_doortag_entity}_connectivity" + + # Check connectivity creation + assert hass.states.get(_doortag_entity_connectivity) is not None + # Check opening creation + assert hass.states.get(_doortag_entity_opening) is not None + + # Check connectivity initial state + assert hass.states.get(_doortag_entity_connectivity).state == "off" + # Check opening initial state + assert hass.states.get(_doortag_entity_opening).state == "unavailable" + + # Trigger some polling cycle to let API throttling work + for _ in range(11): + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Change mocked status + doortag_entity_id = "12:34:56:00:86:99" + doortag_connectivity = True + doortag_opening = doortag_status + doortag_timestamp = int(dt_util.utcnow().timestamp()) + + # Trigger some polling cycle to let status change be picked up + + for _ in range(11): + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Check connectivity mocked state + assert hass.states.get(_doortag_entity_connectivity).state == "on" + # Check opening mocked state + assert hass.states.get(_doortag_entity_opening).state == expected + + +@pytest.mark.parametrize( + ("doortag_category", "expected_key", "expected_class"), + [ + ("door", "door", BinarySensorDeviceClass.DOOR), + ("furniture", "furniture", BinarySensorDeviceClass.OPENING), + ("garage", "garage_door", BinarySensorDeviceClass.GARAGE_DOOR), + ("gate", "gate", BinarySensorDeviceClass.OPENING), + ("other", "opening", BinarySensorDeviceClass.OPENING), + ("window", "window", BinarySensorDeviceClass.WINDOW), + ("invalid_value", "opening", BinarySensorDeviceClass.OPENING), + ], +) +async def test_doortag_opening_category( + hass: HomeAssistant, + config_entry: MockConfigEntry, + netatmo_auth: AsyncMock, + doortag_category: str, + expected_key: str, + expected_class: BinarySensorDeviceClass, +) -> None: + """Test doortag opening status changes.""" + fake_post_hits = 0 + # Repeatedly used variables for the test and initial value from fixture + doortag_entity_id = "12:34:56:00:86:99" + doortag_connectivity = False + doortag_opening = "no_news" + + def tag_modifier(payload): + """This function will be called by common.py during ANY homestatus call.""" + nonlocal doortag_connectivity, doortag_opening + payload["time_server"] = int(dt_util.utcnow().timestamp()) + body = payload.get("body", {}) + + # Handle both structures: {"home": {...}} AND {"homes": [{...}]} + homes_to_check = [] + if "home" in body and isinstance(body["home"], dict): + homes_to_check.append(body["home"]) + elif "homes" in body and isinstance(body["homes"], list): + homes_to_check.extend(body["homes"]) + + for home_data in homes_to_check: + # Safety check: ensure home_data is actually a dictionary + if not isinstance(home_data, dict): + continue + + modules = home_data.get("modules", []) + for module in modules: + if isinstance(module, dict) and module.get("id") == doortag_entity_id: + module["category"] = doortag_category + break + + async def fake_tag_post(*args, **kwargs): + """Fake tag status during requesting backend data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + return await fake_post_request(hass, *args, msg_callback=tag_modifier, **kwargs) + + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, + patch( + "homeassistant.components.netatmo.data_handler.PLATFORMS", + ["camera", "binary_sensor"], + ), + patch( + "homeassistant.components.netatmo.async_get_config_entry_implementation", + return_value=AsyncMock(), + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url", + ) as mock_webhook, + ): + mock_auth.return_value.async_post_api_request.side_effect = fake_tag_post + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + mock_webhook.return_value = "https://example.com" + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + + # Fake webhook activation + await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) + await hass.async_block_till_done() + + # Define the variables for the test + _doortag_entity = "window_hall" + _doortag_entity_opening = f"binary_sensor.{_doortag_entity}_{expected_key}" + + # Check opening creation with right key + assert hass.states.get(_doortag_entity_opening) is not None + # Check opening device class + assert ( + hass.states.get(_doortag_entity_opening).attributes.get("device_class") + == expected_class.value + ) + # Check opening device name + assert ( + hass.states.get(_doortag_entity_opening).attributes.get("friendly_name") + == _doortag_entity.replace("_", " ").title() + + " " + + expected_key.replace("_", " ").capitalize() + ) From 219b982ef583947746fd0b214b5680f675fd3759 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:45:44 +0100 Subject: [PATCH 02/36] Improve type hints in aemet weather (#163239) --- homeassistant/components/aemet/weather.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index 3a17430300d6e0..9b029f9995c921 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -74,7 +74,7 @@ def __init__( self._attr_unique_id = unique_id @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" cond = self.get_aemet_value([AOD_WEATHER, AOD_CONDITION]) return CONDITIONS_MAP.get(cond) @@ -90,31 +90,31 @@ def _async_forecast_hourly(self) -> list[Forecast]: return self.get_aemet_forecast(AOD_FORECAST_HOURLY) @property - def humidity(self): + def humidity(self) -> float | None: """Return the humidity.""" return self.get_aemet_value([AOD_WEATHER, AOD_HUMIDITY]) @property - def native_pressure(self): + def native_pressure(self) -> float | None: """Return the pressure.""" return self.get_aemet_value([AOD_WEATHER, AOD_PRESSURE]) @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the temperature.""" return self.get_aemet_value([AOD_WEATHER, AOD_TEMP]) @property - def wind_bearing(self): + def wind_bearing(self) -> float | None: """Return the wind bearing.""" return self.get_aemet_value([AOD_WEATHER, AOD_WIND_DIRECTION]) @property - def native_wind_gust_speed(self): + def native_wind_gust_speed(self) -> float | None: """Return the wind gust speed in native units.""" return self.get_aemet_value([AOD_WEATHER, AOD_WIND_SPEED_MAX]) @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the wind speed.""" return self.get_aemet_value([AOD_WEATHER, AOD_WIND_SPEED]) From 8e14dc7b5a1337b8aa03352bcf340b9497a017ef Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 17 Feb 2026 10:46:15 +0100 Subject: [PATCH 03/36] Cleanup for 100% coverage of entity for Fritz (#163237) --- homeassistant/components/fritz/entity.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/fritz/entity.py b/homeassistant/components/fritz/entity.py index ef662737ad87f8..ade76993972650 100644 --- a/homeassistant/components/fritz/entity.py +++ b/homeassistant/components/fritz/entity.py @@ -51,11 +51,6 @@ async def async_process_update(self) -> None: """Update device.""" raise NotImplementedError - async def async_on_demand_update(self) -> None: - """Update state.""" - await self.async_process_update() - self.async_write_ha_state() - class FritzBoxBaseEntity: """Fritz host entity base class.""" From 6c0fb12189c9b4cd38627c18e0ad01e94802e834 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:54:33 +0100 Subject: [PATCH 04/36] Improve type hints in ecobee weather (#163240) --- homeassistant/components/ecobee/weather.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 2112842112a46c..8c918db3038fc4 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -28,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util -from . import EcobeeConfigEntry +from . import EcobeeConfigEntry, EcobeeData from .const import ( DOMAIN, ECOBEE_MODEL_TO_NAME, @@ -64,7 +64,7 @@ class EcobeeWeather(WeatherEntity): _attr_name = None _attr_supported_features = WeatherEntityFeature.FORECAST_DAILY - def __init__(self, data, name, index): + def __init__(self, data: EcobeeData, name: str, index: int) -> None: """Initialize the Ecobee weather platform.""" self.data = data self._name = name @@ -99,7 +99,7 @@ def device_info(self) -> DeviceInfo: ) @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" try: return ECOBEE_WEATHER_SYMBOL_TO_HASS[self.get_forecast(0, "weatherSymbol")] @@ -107,7 +107,7 @@ def condition(self): return None @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the temperature.""" try: return float(self.get_forecast(0, "temperature")) / 10 @@ -115,7 +115,7 @@ def native_temperature(self): return None @property - def native_pressure(self): + def native_pressure(self) -> float | None: """Return the pressure.""" try: pressure = self.get_forecast(0, "pressure") @@ -124,7 +124,7 @@ def native_pressure(self): return None @property - def humidity(self): + def humidity(self) -> float | None: """Return the humidity.""" try: return int(self.get_forecast(0, "relativeHumidity")) @@ -132,7 +132,7 @@ def humidity(self): return None @property - def native_visibility(self): + def native_visibility(self) -> float | None: """Return the visibility.""" try: return int(self.get_forecast(0, "visibility")) @@ -140,7 +140,7 @@ def native_visibility(self): return None @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the wind speed.""" try: return int(self.get_forecast(0, "windSpeed")) @@ -148,7 +148,7 @@ def native_wind_speed(self): return None @property - def wind_bearing(self): + def wind_bearing(self) -> float | None: """Return the wind direction.""" try: return int(self.get_forecast(0, "windBearing")) @@ -156,7 +156,7 @@ def wind_bearing(self): return None @property - def attribution(self): + def attribution(self) -> str | None: """Return the attribution.""" if not self.weather: return None @@ -167,7 +167,7 @@ def attribution(self): def _forecast(self) -> list[Forecast] | None: """Return the forecast array.""" - if "forecasts" not in self.weather: + if not self.weather or "forecasts" not in self.weather: return None forecasts: list[Forecast] = [] From f0e7d099e6a6f2cb297488adfcc4df76036751be Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:55:00 +0100 Subject: [PATCH 05/36] Improve type hints in environment_canada weather (#163241) --- .../components/environment_canada/weather.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index a5acb224bd0bd0..c7d04e4c03d88e 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -123,7 +123,7 @@ def __init__(self, coordinator: ECDataUpdateCoordinator[ECWeather]) -> None: self._attr_device_info = coordinator.device_info @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the temperature.""" if ( temperature := self.ec_data.conditions.get("temperature", {}).get("value") @@ -138,42 +138,42 @@ def native_temperature(self): return None @property - def humidity(self): + def humidity(self) -> float | None: """Return the humidity.""" if self.ec_data.conditions.get("humidity", {}).get("value"): return float(self.ec_data.conditions["humidity"]["value"]) return None @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the wind speed.""" if self.ec_data.conditions.get("wind_speed", {}).get("value"): return float(self.ec_data.conditions["wind_speed"]["value"]) return None @property - def wind_bearing(self): + def wind_bearing(self) -> float | None: """Return the wind bearing.""" if self.ec_data.conditions.get("wind_bearing", {}).get("value"): return float(self.ec_data.conditions["wind_bearing"]["value"]) return None @property - def native_pressure(self): + def native_pressure(self) -> float | None: """Return the pressure.""" if self.ec_data.conditions.get("pressure", {}).get("value"): return float(self.ec_data.conditions["pressure"]["value"]) return None @property - def native_visibility(self): + def native_visibility(self) -> float | None: """Return the visibility.""" if self.ec_data.conditions.get("visibility", {}).get("value"): return float(self.ec_data.conditions["visibility"]["value"]) return None @property - def condition(self): + def condition(self) -> str | None: """Return the weather condition.""" icon_code = None @@ -186,7 +186,7 @@ def condition(self): if icon_code: return icon_code_to_condition(int(icon_code)) - return "" + return None @callback def _async_forecast_daily(self) -> list[Forecast] | None: @@ -261,7 +261,7 @@ def get_day_forecast( return forecast_array -def icon_code_to_condition(icon_code): +def icon_code_to_condition(icon_code: int) -> str | None: """Return the condition corresponding to an icon code.""" for condition, codes in ICON_CONDITION_MAP.items(): if icon_code in codes: From 6c50711e2b4b102be82726d6c33c4d40038df365 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:55:20 +0100 Subject: [PATCH 06/36] Improve type hints in ipma weather (#163242) --- homeassistant/components/ipma/weather.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 74344da8affb33..02689a4b791eaa 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -129,7 +129,7 @@ def _condition_conversion(self, identifier, forecast_dt): return CONDITION_MAP.get(identifier) @property - def condition(self): + def condition(self) -> str | None: """Return the current condition which is only available on the hourly forecast data.""" forecast = self._hourly_forecast @@ -139,7 +139,7 @@ def condition(self): return self._condition_conversion(forecast[0].weather_type.id, None) @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the current temperature.""" if not self._observation: return None @@ -147,7 +147,7 @@ def native_temperature(self): return self._observation.temperature @property - def native_pressure(self): + def native_pressure(self) -> float | None: """Return the current pressure.""" if not self._observation: return None @@ -155,7 +155,7 @@ def native_pressure(self): return self._observation.pressure @property - def humidity(self): + def humidity(self) -> float | None: """Return the name of the sensor.""" if not self._observation: return None @@ -163,7 +163,7 @@ def humidity(self): return self._observation.humidity @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the current windspeed.""" if not self._observation: return None @@ -171,7 +171,7 @@ def native_wind_speed(self): return self._observation.wind_intensity_km @property - def wind_bearing(self): + def wind_bearing(self) -> float | None: """Return the current wind bearing (degrees).""" if not self._observation: return None From 7f65db260f79ba3b99402d6b8a7ffb75a661ded1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:55:51 +0100 Subject: [PATCH 07/36] Improve type hints in meteo_france weather (#163243) --- .../components/meteo_france/weather.py | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 9b3472e3312dd1..ed8abec33f815f 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -105,9 +105,9 @@ def __init__( ) -> None: """Initialise the platform with a data instance and station name.""" super().__init__(coordinator) - self._city_name = self.coordinator.data.position["name"] + self._attr_name = self.coordinator.data.position["name"] self._mode = mode - self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}" + self._attr_unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}" @callback def _handle_coordinator_update(self) -> None: @@ -118,16 +118,6 @@ def _handle_coordinator_update(self) -> None: self.hass, self.async_update_listeners(("daily", "hourly")) ) - @property - def unique_id(self) -> str: - """Return the unique id of the sensor.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._city_name - @property def device_info(self) -> DeviceInfo: """Return the device info.""" @@ -141,39 +131,39 @@ def device_info(self) -> DeviceInfo: ) @property - def condition(self): + def condition(self) -> str: """Return the current condition.""" return format_condition( self.coordinator.data.current_forecast["weather"]["desc"] ) @property - def native_temperature(self): + def native_temperature(self) -> float: """Return the temperature.""" return self.coordinator.data.current_forecast["T"]["value"] @property - def native_pressure(self): + def native_pressure(self) -> float: """Return the pressure.""" return self.coordinator.data.current_forecast["sea_level"] @property - def humidity(self): + def humidity(self) -> float: """Return the humidity.""" return self.coordinator.data.current_forecast["humidity"] @property - def native_wind_speed(self): + def native_wind_speed(self) -> float: """Return the wind speed.""" return self.coordinator.data.current_forecast["wind"]["speed"] @property - def native_wind_gust_speed(self): + def native_wind_gust_speed(self) -> float | None: """Return the wind gust speed.""" return self.coordinator.data.current_forecast["wind"].get("gust") @property - def wind_bearing(self): + def wind_bearing(self) -> float | None: """Return the wind bearing.""" wind_bearing = self.coordinator.data.current_forecast["wind"]["direction"] if wind_bearing != -1: From 632218520681c441703c392eca8cf19b6e6aed4a Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 17 Feb 2026 10:56:32 +0100 Subject: [PATCH 08/36] Bump onedrive-personal-sdk to 0.1.4 (#163238) --- homeassistant/components/onedrive/manifest.json | 2 +- homeassistant/components/onedrive_for_business/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onedrive/manifest.json b/homeassistant/components/onedrive/manifest.json index 20cd867055f1be..e6e9901365fb78 100644 --- a/homeassistant/components/onedrive/manifest.json +++ b/homeassistant/components/onedrive/manifest.json @@ -10,5 +10,5 @@ "iot_class": "cloud_polling", "loggers": ["onedrive_personal_sdk"], "quality_scale": "platinum", - "requirements": ["onedrive-personal-sdk==0.1.2"] + "requirements": ["onedrive-personal-sdk==0.1.4"] } diff --git a/homeassistant/components/onedrive_for_business/manifest.json b/homeassistant/components/onedrive_for_business/manifest.json index e398cefa12ade3..42ec77be274cd5 100644 --- a/homeassistant/components/onedrive_for_business/manifest.json +++ b/homeassistant/components/onedrive_for_business/manifest.json @@ -10,5 +10,5 @@ "iot_class": "cloud_polling", "loggers": ["onedrive_personal_sdk"], "quality_scale": "bronze", - "requirements": ["onedrive-personal-sdk==0.1.2"] + "requirements": ["onedrive-personal-sdk==0.1.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 55bdf620cd8f1a..33939a98d47ad6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1670,7 +1670,7 @@ ondilo==0.5.0 # homeassistant.components.onedrive # homeassistant.components.onedrive_for_business -onedrive-personal-sdk==0.1.2 +onedrive-personal-sdk==0.1.4 # homeassistant.components.onvif onvif-zeep-async==4.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c8c5ab7b1ecbb..183cd008f2f61c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1456,7 +1456,7 @@ ondilo==0.5.0 # homeassistant.components.onedrive # homeassistant.components.onedrive_for_business -onedrive-personal-sdk==0.1.2 +onedrive-personal-sdk==0.1.4 # homeassistant.components.onvif onvif-zeep-async==4.0.4 From 487e2f8ccc95ad957c53c4cab1b64c97e2b225fb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:07:48 +0100 Subject: [PATCH 09/36] Improve type hints in tomorrowio weather (#163246) --- homeassistant/components/tomorrowio/entity.py | 4 +++- homeassistant/components/tomorrowio/weather.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tomorrowio/entity.py b/homeassistant/components/tomorrowio/entity.py index 6560ac58724dc3..f00677b1561bce 100644 --- a/homeassistant/components/tomorrowio/entity.py +++ b/homeassistant/components/tomorrowio/entity.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from pytomorrowio.const import CURRENT from homeassistant.config_entries import ConfigEntry @@ -36,7 +38,7 @@ def __init__( entry_type=DeviceEntryType.SERVICE, ) - def _get_current_property(self, property_name: str) -> int | str | float | None: + def _get_current_property(self, property_name: str) -> Any | None: """Get property from current conditions. Used for V4 API. diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index 0a070a1b33b679..36b85515c3c215 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -175,37 +175,37 @@ def _translate_condition( return CONDITIONS[condition] @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the platform temperature.""" return self._get_current_property(TMRW_ATTR_TEMPERATURE) @property - def native_pressure(self): + def native_pressure(self) -> float | None: """Return the raw pressure.""" return self._get_current_property(TMRW_ATTR_PRESSURE) @property - def humidity(self): + def humidity(self) -> float | None: """Return the humidity.""" return self._get_current_property(TMRW_ATTR_HUMIDITY) @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the raw wind speed.""" return self._get_current_property(TMRW_ATTR_WIND_SPEED) @property - def wind_bearing(self): + def wind_bearing(self) -> float | None: """Return the wind bearing.""" return self._get_current_property(TMRW_ATTR_WIND_DIRECTION) @property - def ozone(self): + def ozone(self) -> float | None: """Return the O3 (ozone) level.""" return self._get_current_property(TMRW_ATTR_OZONE) @property - def condition(self): + def condition(self) -> str | None: """Return the condition.""" return self._translate_condition( self._get_current_property(TMRW_ATTR_CONDITION), @@ -213,7 +213,7 @@ def condition(self): ) @property - def native_visibility(self): + def native_visibility(self) -> float | None: """Return the raw visibility.""" return self._get_current_property(TMRW_ATTR_VISIBILITY) From 68c82c2f908668a1a729b5dc9657c8fba83a5363 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 17 Feb 2026 11:23:54 +0100 Subject: [PATCH 10/36] Debug logging for service calls (#163235) --- homeassistant/components/websocket_api/commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 5643f07e7ee0f1..e083a8253b14a6 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -332,6 +332,7 @@ async def handle_call_service( connection.logger.error( "Error during service call to %s.%s: %s", msg["domain"], msg["service"], err ) + connection.logger.debug("", exc_info=True) connection.send_error( msg["id"], const.ERR_HOME_ASSISTANT_ERROR, From f1c142b3d3af755ad3061ba03ec672ec1bb33b80 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Tue, 17 Feb 2026 11:40:25 +0100 Subject: [PATCH 11/36] Refactor BSB-Lan tests (#163245) --- tests/components/bsblan/conftest.py | 14 -------------- tests/components/bsblan/test_diagnostics.py | 7 +++++-- tests/components/bsblan/test_init.py | 2 -- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index 31b0b15b443f81..9a6865706fd37c 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -17,7 +17,6 @@ from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME -from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -81,16 +80,3 @@ def mock_bsblan() -> Generator[MagicMock]: bsblan.get_temperature_unit = "°C" yield bsblan - - -@pytest.fixture -async def init_integration( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_bsblan: MagicMock -) -> MockConfigEntry: - """Set up the bsblan integration for testing.""" - mock_config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - return mock_config_entry diff --git a/tests/components/bsblan/test_diagnostics.py b/tests/components/bsblan/test_diagnostics.py index c6b6c92e71899d..05bcb1e7c03a61 100644 --- a/tests/components/bsblan/test_diagnostics.py +++ b/tests/components/bsblan/test_diagnostics.py @@ -15,12 +15,15 @@ async def test_diagnostics( hass: HomeAssistant, mock_bsblan: AsyncMock, hass_client: ClientSessionGenerator, - init_integration: MockConfigEntry, + mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diagnostics_data = await get_diagnostics_for_config_entry( - hass, hass_client, init_integration + hass, hass_client, mock_config_entry ) assert diagnostics_data == snapshot diff --git a/tests/components/bsblan/test_init.py b/tests/components/bsblan/test_init.py index b6c511f6b7264b..c2d44c0b0cda7f 100644 --- a/tests/components/bsblan/test_init.py +++ b/tests/components/bsblan/test_init.py @@ -6,7 +6,6 @@ from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.bsblan.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -29,7 +28,6 @@ async def test_load_unload_config_entry( await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() - assert not hass.data.get(DOMAIN) assert mock_config_entry.state is ConfigEntryState.NOT_LOADED From 34a78f925163b4103f3112301b46b351f13ce6f5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:15:03 +0100 Subject: [PATCH 12/36] Rename DOMAIN aliases (#163253) --- homeassistant/components/accuweather/__init__.py | 4 ++-- homeassistant/components/airly/__init__.py | 6 ++---- homeassistant/components/brother/sensor.py | 4 ++-- homeassistant/components/gios/sensor.py | 4 ++-- homeassistant/components/hdmi_cec/__init__.py | 6 +++--- homeassistant/components/imgw_pib/__init__.py | 4 ++-- homeassistant/components/imgw_pib/sensor.py | 4 ++-- .../components/mikrotik/device_tracker.py | 4 ++-- homeassistant/components/nam/__init__.py | 4 ++-- homeassistant/components/nam/sensor.py | 6 ++++-- homeassistant/components/plaato/__init__.py | 4 ++-- homeassistant/components/shelly/binary_sensor.py | 6 +++--- homeassistant/components/shelly/button.py | 8 ++++---- homeassistant/components/shelly/number.py | 8 ++++---- homeassistant/components/shelly/select.py | 8 ++++---- homeassistant/components/shelly/sensor.py | 10 +++++----- homeassistant/components/shelly/switch.py | 14 +++++++------- homeassistant/components/shelly/text.py | 10 ++++------ homeassistant/components/tractive/__init__.py | 4 ++-- 19 files changed, 58 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index bb453c67f57f82..de8f2ab93a9bb6 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -7,7 +7,7 @@ from accuweather import AccuWeather -from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -72,7 +72,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) ent_reg = er.async_get(hass) for day in range(5): unique_id = f"{location_key}-ozone-{day}" - if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id): + if entity_id := ent_reg.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, unique_id): _LOGGER.debug("Removing ozone sensor entity %s", entity_id) ent_reg.async_remove(entity_id) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 18ad1c8c402b65..7c26f6062d6266 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -5,7 +5,7 @@ from datetime import timedelta import logging -from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -75,9 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirlyConfigEntry) -> boo # Remove air_quality entities from registry if they exist ent_reg = er.async_get(hass) unique_id = f"{coordinator.latitude}-{coordinator.longitude}" - if entity_id := ent_reg.async_get_entity_id( - AIR_QUALITY_PLATFORM, DOMAIN, unique_id - ): + if entity_id := ent_reg.async_get_entity_id(AIR_QUALITY_DOMAIN, DOMAIN, unique_id): _LOGGER.debug("Removing deprecated air_quality entity %s", entity_id) ent_reg.async_remove(entity_id) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index dda4231dd30960..4f1a10c26213c1 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -10,7 +10,7 @@ from brother import BrotherSensors from homeassistant.components.sensor import ( - DOMAIN as PLATFORM, + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -314,7 +314,7 @@ async def async_setup_entry( entity_registry = er.async_get(hass) old_unique_id = f"{coordinator.brother.serial.lower()}_b/w_counter" if entity_id := entity_registry.async_get_entity_id( - PLATFORM, DOMAIN, old_unique_id + SENSOR_DOMAIN, DOMAIN, old_unique_id ): new_unique_id = f"{coordinator.brother.serial.lower()}_bw_counter" _LOGGER.debug( diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index b51526ebcaf665..5304fb98cf246d 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -9,7 +9,7 @@ from gios.model import GiosSensors from homeassistant.components.sensor import ( - DOMAIN as PLATFORM, + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -187,7 +187,7 @@ async def async_setup_entry( entity_registry = er.async_get(hass) old_unique_id = f"{coordinator.gios.station_id}-pm2.5" if entity_id := entity_registry.async_get_entity_id( - PLATFORM, DOMAIN, old_unique_id + SENSOR_DOMAIN, DOMAIN, old_unique_id ): new_unique_id = f"{coordinator.gios.station_id}-{ATTR_PM25}" _LOGGER.debug( diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 3e31dd73b5d435..3a7f07081e2878 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -23,7 +23,7 @@ from pycec.tcp import TcpAdapter import voluptuous as vol -from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER +from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( CONF_DEVICES, @@ -122,11 +122,11 @@ vol.Optional(CONF_DEVICES): vol.Any( DEVICE_SCHEMA, vol.Schema({vol.All(cv.string): vol.Any(cv.string)}) ), - vol.Optional(CONF_PLATFORM): vol.Any(SWITCH, MEDIA_PLAYER), + vol.Optional(CONF_PLATFORM): vol.Any(SWITCH, MEDIA_PLAYER_DOMAIN), vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_DISPLAY_NAME): cv.string, vol.Optional(CONF_TYPES, default={}): vol.Schema( - {cv.entity_id: vol.Any(MEDIA_PLAYER, SWITCH)} + {cv.entity_id: vol.Any(MEDIA_PLAYER_DOMAIN, SWITCH)} ), } ) diff --git a/homeassistant/components/imgw_pib/__init__.py b/homeassistant/components/imgw_pib/__init__.py index 4bceee51f8e911..f2d30ce34efd34 100644 --- a/homeassistant/components/imgw_pib/__init__.py +++ b/homeassistant/components/imgw_pib/__init__.py @@ -8,7 +8,7 @@ from imgw_pib import ImgwPib from imgw_pib.exceptions import ApiError -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_PLATFORM +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ImgwPibConfigEntry) -> b entity_reg = er.async_get(hass) for key in ("flood_warning", "flood_alarm"): if entity_id := entity_reg.async_get_entity_id( - BINARY_SENSOR_PLATFORM, DOMAIN, f"{coordinator.station_id}_{key}" + BINARY_SENSOR_DOMAIN, DOMAIN, f"{coordinator.station_id}_{key}" ): entity_reg.async_remove(entity_id) diff --git a/homeassistant/components/imgw_pib/sensor.py b/homeassistant/components/imgw_pib/sensor.py index 7084889220c0e4..170736d8f6c972 100644 --- a/homeassistant/components/imgw_pib/sensor.py +++ b/homeassistant/components/imgw_pib/sensor.py @@ -10,7 +10,7 @@ from imgw_pib.model import HydrologicalData from homeassistant.components.sensor import ( - DOMAIN as SENSOR_PLATFORM, + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -102,7 +102,7 @@ async def async_setup_entry( entity_reg = er.async_get(hass) for key in ("flood_warning_level", "flood_alarm_level"): if entity_id := entity_reg.async_get_entity_id( - SENSOR_PLATFORM, DOMAIN, f"{coordinator.station_id}_{key}" + SENSOR_DOMAIN, DOMAIN, f"{coordinator.station_id}_{key}" ): entity_reg.async_remove(entity_id) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index f7bc10e31d463e..b166a3a182ac74 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER, + DOMAIN as DEVICE_TRACKER_DOMAIN, ScannerEntity, ) from homeassistant.core import HomeAssistant, callback @@ -33,7 +33,7 @@ async def async_setup_entry( for entity in registry.entities.get_entries_for_config_entry_id( config_entry.entry_id ): - if entity.domain == DEVICE_TRACKER: + if entity.domain == DEVICE_TRACKER_DOMAIN: if ( entity.unique_id in coordinator.api.devices or entity.unique_id not in coordinator.api.all_devices diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 03ad5118352499..4504cff42b3a92 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -12,7 +12,7 @@ NettigoAirMonitor, ) -from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: NAMConfigEntry) -> bool: for sensor_type in ("sds", ATTR_SDS011, ATTR_SPS30): unique_id = f"{coordinator.unique_id}-{sensor_type}" if entity_id := ent_reg.async_get_entity_id( - AIR_QUALITY_PLATFORM, DOMAIN, unique_id + AIR_QUALITY_DOMAIN, DOMAIN, unique_id ): _LOGGER.debug("Removing deprecated air_quality entity %s", entity_id) ent_reg.async_remove(entity_id) diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index a7e5eb71912867..e59d111e5e553d 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -10,7 +10,7 @@ from nettigo_air_monitor import NAMSensors from homeassistant.components.sensor import ( - DOMAIN as PLATFORM, + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -381,7 +381,9 @@ async def async_setup_entry( for old_sensor, new_sensor in MIGRATION_SENSORS: old_unique_id = f"{coordinator.unique_id}-{old_sensor}" new_unique_id = f"{coordinator.unique_id}-{new_sensor}" - if entity_id := ent_reg.async_get_entity_id(PLATFORM, DOMAIN, old_unique_id): + if entity_id := ent_reg.async_get_entity_id( + SENSOR_DOMAIN, DOMAIN, old_unique_id + ): _LOGGER.debug( "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", entity_id, diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 14e757d46233d2..490bc094aaaca8 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -22,7 +22,7 @@ import voluptuous as vol from homeassistant.components import webhook -from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_SCAN_INTERVAL, @@ -57,7 +57,7 @@ DEPENDENCIES = ["webhook"] SENSOR_UPDATE = f"{DOMAIN}_sensor_update" -SENSOR_DATA_KEY = f"{DOMAIN}.{SENSOR}" +SENSOR_DATA_KEY = f"{DOMAIN}.{SENSOR_DOMAIN}" WEBHOOK_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index ca93da3ee1fd85..632e5277de5f2e 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -8,7 +8,7 @@ from aioshelly.const import MODEL_FLOOD_G4, RPC_GENERATIONS from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR_PLATFORM, + DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, @@ -292,7 +292,7 @@ def __init__( key="boolean", sub_key="value", removal_condition=lambda config, _, key: ( - not is_view_for_platform(config, key, BINARY_SENSOR_PLATFORM) + not is_view_for_platform(config, key, BINARY_SENSOR_DOMAIN) ), role=ROLE_GENERIC, ), @@ -424,7 +424,7 @@ def _async_setup_rpc_entry( hass, config_entry.entry_id, coordinator.mac, - BINARY_SENSOR_PLATFORM, + BINARY_SENSOR_DOMAIN, coordinator.device.status, ) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 7ee8794e509216..9fb3cb895160b7 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -11,7 +11,7 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError from homeassistant.components.button import ( - DOMAIN as BUTTON_PLATFORM, + DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass, ButtonEntity, ButtonEntityDescription, @@ -217,7 +217,7 @@ async def async_setup_entry( # added in https://github.com/home-assistant/core/pull/154673 entry_sleep_period = config_entry.data[CONF_SLEEP_PERIOD] if device_gen in RPC_GENERATIONS and entry_sleep_period: - async_remove_shelly_entity(hass, BUTTON_PLATFORM, f"{coordinator.mac}-reboot") + async_remove_shelly_entity(hass, BUTTON_DOMAIN, f"{coordinator.mac}-reboot") entities: list[ShellyButton] = [] @@ -249,13 +249,13 @@ async def async_setup_entry( # the user can remove virtual components from the device configuration, so # we need to remove orphaned entities virtual_button_component_ids = get_virtual_component_ids( - coordinator.device.config, BUTTON_PLATFORM + coordinator.device.config, BUTTON_DOMAIN ) async_remove_orphaned_entities( hass, config_entry.entry_id, coordinator.mac, - BUTTON_PLATFORM, + BUTTON_DOMAIN, virtual_button_component_ids, ) diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index 857c79a0335aa1..305dd5ebd70d4d 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -11,7 +11,7 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from homeassistant.components.number import ( - DOMAIN as NUMBER_PLATFORM, + DOMAIN as NUMBER_DOMAIN, NumberDeviceClass, NumberEntity, NumberEntityDescription, @@ -210,7 +210,7 @@ async def async_set_native_value(self, value: float) -> None: key="number", sub_key="value", removal_condition=lambda config, _, key: ( - not is_view_for_platform(config, key, NUMBER_PLATFORM) + not is_view_for_platform(config, key, NUMBER_DOMAIN) ), max_fn=lambda config: config["max"], min_fn=lambda config: config["min"], @@ -380,13 +380,13 @@ def _async_setup_rpc_entry( # the user can remove virtual components from the device configuration, so # we need to remove orphaned entities virtual_number_ids = get_virtual_component_ids( - coordinator.device.config, NUMBER_PLATFORM + coordinator.device.config, NUMBER_DOMAIN ) async_remove_orphaned_entities( hass, config_entry.entry_id, coordinator.mac, - NUMBER_PLATFORM, + NUMBER_DOMAIN, virtual_number_ids, "number", ) diff --git a/homeassistant/components/shelly/select.py b/homeassistant/components/shelly/select.py index afc86f4a54e3ed..262efcd01ee758 100644 --- a/homeassistant/components/shelly/select.py +++ b/homeassistant/components/shelly/select.py @@ -8,7 +8,7 @@ from aioshelly.const import RPC_GENERATIONS from homeassistant.components.select import ( - DOMAIN as SELECT_PLATFORM, + DOMAIN as SELECT_DOMAIN, SelectEntity, SelectEntityDescription, ) @@ -117,7 +117,7 @@ def current_option(self) -> str | None: key="enum", sub_key="value", removal_condition=lambda config, _status, key: ( - not is_view_for_platform(config, key, SELECT_PLATFORM) + not is_view_for_platform(config, key, SELECT_DOMAIN) ), method="enum_set", role=ROLE_GENERIC, @@ -154,13 +154,13 @@ def _async_setup_rpc_entry( # the user can remove virtual components from the device configuration, so # we need to remove orphaned entities virtual_text_ids = get_virtual_component_ids( - coordinator.device.config, SELECT_PLATFORM + coordinator.device.config, SELECT_DOMAIN ) async_remove_orphaned_entities( hass, config_entry.entry_id, coordinator.mac, - SELECT_PLATFORM, + SELECT_DOMAIN, virtual_text_ids, "enum", ) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index b7cc317cb9d3ca..41d710cf2da082 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -9,7 +9,7 @@ from aioshelly.const import RPC_GENERATIONS from homeassistant.components.sensor import ( - DOMAIN as SENSOR_PLATFORM, + DOMAIN as SENSOR_DOMAIN, RestoreSensor, SensorDeviceClass, SensorEntity, @@ -1357,7 +1357,7 @@ def __init__( key="text", sub_key="value", removal_condition=lambda config, _, key: ( - not is_view_for_platform(config, key, SENSOR_PLATFORM) + not is_view_for_platform(config, key, SENSOR_DOMAIN) ), role=ROLE_GENERIC, ), @@ -1365,7 +1365,7 @@ def __init__( key="number", sub_key="value", removal_condition=lambda config, _, key: ( - not is_view_for_platform(config, key, SENSOR_PLATFORM) + not is_view_for_platform(config, key, SENSOR_DOMAIN) ), unit=get_virtual_component_unit, role=ROLE_GENERIC, @@ -1374,7 +1374,7 @@ def __init__( key="enum", sub_key="value", removal_condition=lambda config, _, key: ( - not is_view_for_platform(config, key, SENSOR_PLATFORM) + not is_view_for_platform(config, key, SENSOR_DOMAIN) ), device_class=SensorDeviceClass.ENUM, role=ROLE_GENERIC, @@ -1792,7 +1792,7 @@ def _async_setup_rpc_entry( hass, config_entry.entry_id, coordinator.mac, - SENSOR_PLATFORM, + SENSOR_DOMAIN, coordinator.device.status, ) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 3cad237bc9a83a..5a4f8debd1b430 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -9,9 +9,9 @@ from aioshelly.block_device import Block from aioshelly.const import RPC_GENERATIONS -from homeassistant.components.climate import DOMAIN as CLIMATE_PLATFORM +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.switch import ( - DOMAIN as SWITCH_PLATFORM, + DOMAIN as SWITCH_DOMAIN, SwitchEntity, SwitchEntityDescription, ) @@ -101,7 +101,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): key="boolean", sub_key="value", removal_condition=lambda config, _, key: ( - not is_view_for_platform(config, key, SWITCH_PLATFORM) + not is_view_for_platform(config, key, SWITCH_DOMAIN) ), is_on=lambda status: bool(status["value"]), method_on="boolean_set", @@ -379,13 +379,13 @@ def _async_setup_rpc_entry( # the user can remove virtual components from the device configuration, so we need # to remove orphaned entities virtual_switch_ids = get_virtual_component_ids( - coordinator.device.config, SWITCH_PLATFORM + coordinator.device.config, SWITCH_DOMAIN ) async_remove_orphaned_entities( hass, config_entry.entry_id, coordinator.mac, - SWITCH_PLATFORM, + SWITCH_DOMAIN, virtual_switch_ids, "boolean", ) @@ -396,7 +396,7 @@ def _async_setup_rpc_entry( hass, config_entry.entry_id, coordinator.mac, - SWITCH_PLATFORM, + SWITCH_DOMAIN, coordinator.device.status, "script", ) @@ -407,7 +407,7 @@ def _async_setup_rpc_entry( hass, config_entry.entry_id, coordinator.mac, - CLIMATE_PLATFORM, + CLIMATE_DOMAIN, coordinator.device.status, "thermostat", ) diff --git a/homeassistant/components/shelly/text.py b/homeassistant/components/shelly/text.py index 2ba043e5c2801a..4d526f65a7e9ea 100644 --- a/homeassistant/components/shelly/text.py +++ b/homeassistant/components/shelly/text.py @@ -8,7 +8,7 @@ from aioshelly.const import RPC_GENERATIONS from homeassistant.components.text import ( - DOMAIN as TEXT_PLATFORM, + DOMAIN as TEXT_DOMAIN, TextEntity, TextEntityDescription, ) @@ -44,7 +44,7 @@ class RpcTextDescription(RpcEntityDescription, TextEntityDescription): key="text", sub_key="value", removal_condition=lambda config, _status, key: ( - not is_view_for_platform(config, key, TEXT_PLATFORM) + not is_view_for_platform(config, key, TEXT_DOMAIN) ), role=ROLE_GENERIC, ), @@ -79,14 +79,12 @@ def _async_setup_rpc_entry( # the user can remove virtual components from the device configuration, so # we need to remove orphaned entities - virtual_text_ids = get_virtual_component_ids( - coordinator.device.config, TEXT_PLATFORM - ) + virtual_text_ids = get_virtual_component_ids(coordinator.device.config, TEXT_DOMAIN) async_remove_orphaned_entities( hass, config_entry.entry_id, coordinator.mac, - TEXT_PLATFORM, + TEXT_DOMAIN, virtual_text_ids, "text", ) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index a8e0f451d09c0f..e5c20e757eaef5 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -9,7 +9,7 @@ import aiotractive -from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_CHARGING, @@ -136,7 +136,7 @@ async def cancel_listen_task(_: Event) -> None: for item in filtered_trackables: for key in ("activity_label", "calories", "sleep_label"): if entity_id := entity_reg.async_get_entity_id( - SENSOR_PLATFORM, DOMAIN, f"{item.trackable['_id']}_{key}" + SENSOR_DOMAIN, DOMAIN, f"{item.trackable['_id']}_{key}" ): entity_reg.async_remove(entity_id) From 82148e46f52440cdcf9170dc20e44cf422ca1c2d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:15:36 +0100 Subject: [PATCH 13/36] Rename DOMAIN aliases in tests (#163254) --- tests/components/airly/test_init.py | 4 ++-- tests/components/imgw_pib/test_init.py | 4 ++-- tests/components/imgw_pib/test_sensor.py | 4 ++-- tests/components/nam/test_init.py | 6 +++--- tests/components/nibe_heatpump/test_switch.py | 6 +++--- tests/components/shelly/test_select.py | 20 +++++++++---------- tests/components/shelly/test_text.py | 16 +++++++-------- tests/components/tractive/test_init.py | 4 ++-- tests/components/weheat/conftest.py | 4 ++-- 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index b7fa8a44360098..ea24fe80c0aa03 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -5,7 +5,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_DOMAIN from homeassistant.components.airly.const import DOMAIN from homeassistant.components.airly.coordinator import set_update_interval from homeassistant.config_entries import ConfigEntryState @@ -245,7 +245,7 @@ async def test_remove_air_quality_entities( ) -> None: """Test remove air_quality entities from registry.""" entity_registry.async_get_or_create( - AIR_QUALITY_PLATFORM, + AIR_QUALITY_DOMAIN, DOMAIN, "123-456", suggested_object_id="home", diff --git a/tests/components/imgw_pib/test_init.py b/tests/components/imgw_pib/test_init.py index e352c643676658..20fe4aed1aaa72 100644 --- a/tests/components/imgw_pib/test_init.py +++ b/tests/components/imgw_pib/test_init.py @@ -4,7 +4,7 @@ from imgw_pib import ApiError -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_PLATFORM +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.imgw_pib.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -58,7 +58,7 @@ async def test_remove_binary_sensor_entity( mock_config_entry.add_to_hass(hass) entity_registry.async_get_or_create( - BINARY_SENSOR_PLATFORM, + BINARY_SENSOR_DOMAIN, DOMAIN, "123_flood_alarm", suggested_object_id=entity_id.rsplit(".", maxsplit=1)[-1], diff --git a/tests/components/imgw_pib/test_sensor.py b/tests/components/imgw_pib/test_sensor.py index cb27f0f9b46590..48d03df5786761 100644 --- a/tests/components/imgw_pib/test_sensor.py +++ b/tests/components/imgw_pib/test_sensor.py @@ -8,7 +8,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.imgw_pib.const import DOMAIN, UPDATE_INTERVAL -from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -79,7 +79,7 @@ async def test_remove_entity( mock_config_entry.add_to_hass(hass) entity_registry.async_get_or_create( - SENSOR_PLATFORM, + SENSOR_DOMAIN, DOMAIN, "123_flood_alarm_level", suggested_object_id=entity_id.rsplit(".", maxsplit=1)[-1], diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index ea61739c008fa8..878d90e19ceb74 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -4,7 +4,7 @@ from nettigo_air_monitor import ApiError, AuthFailedError -from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_DOMAIN from homeassistant.components.nam.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE @@ -81,7 +81,7 @@ async def test_remove_air_quality_entities( ) -> None: """Test remove air_quality entities from registry.""" entity_registry.async_get_or_create( - AIR_QUALITY_PLATFORM, + AIR_QUALITY_DOMAIN, DOMAIN, "aa:bb:cc:dd:ee:ff-sds011", suggested_object_id="nettigo_air_monitor_sds011", @@ -89,7 +89,7 @@ async def test_remove_air_quality_entities( ) entity_registry.async_get_or_create( - AIR_QUALITY_PLATFORM, + AIR_QUALITY_DOMAIN, DOMAIN, "aa:bb:cc:dd:ee:ff-sps30", suggested_object_id="nettigo_air_monitor_sps30", diff --git a/tests/components/nibe_heatpump/test_switch.py b/tests/components/nibe_heatpump/test_switch.py index 4221de52ba121f..76e34f6daa16f4 100644 --- a/tests/components/nibe_heatpump/test_switch.py +++ b/tests/components/nibe_heatpump/test_switch.py @@ -9,7 +9,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( - DOMAIN as SWITCH_PLATFORM, + DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) @@ -79,7 +79,7 @@ async def test_turn_on( # Write value await hass.services.async_call( - SWITCH_PLATFORM, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True, @@ -118,7 +118,7 @@ async def test_turn_off( # Write value await hass.services.async_call( - SWITCH_PLATFORM, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True, diff --git a/tests/components/shelly/test_select.py b/tests/components/shelly/test_select.py index 26bb3a9cd78691..dce5f151f2cfae 100644 --- a/tests/components/shelly/test_select.py +++ b/tests/components/shelly/test_select.py @@ -9,7 +9,7 @@ from homeassistant.components.select import ( ATTR_OPTION, ATTR_OPTIONS, - DOMAIN as SELECT_PLATFORM, + DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) from homeassistant.components.shelly.const import DOMAIN @@ -91,7 +91,7 @@ async def test_rpc_device_virtual_enum( monkeypatch.setitem(mock_rpc_device.status["enum:203"], "value", "option 1") await hass.services.async_call( - SELECT_PLATFORM, + SELECT_DOMAIN, SERVICE_SELECT_OPTION, {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Title 1"}, blocking=True, @@ -131,7 +131,7 @@ async def test_rpc_remove_virtual_enum_when_mode_label( device_entry = register_device(device_registry, config_entry) entity_id = register_entity( hass, - SELECT_PLATFORM, + SELECT_DOMAIN, "test_name_enum_200", "enum:200-enum_generic", config_entry, @@ -155,7 +155,7 @@ async def test_rpc_remove_virtual_enum_when_orphaned( device_entry = register_device(device_registry, config_entry) entity_id = register_entity( hass, - SELECT_PLATFORM, + SELECT_DOMAIN, "test_name_enum_200", "enum:200-enum_generic", config_entry, @@ -212,10 +212,10 @@ async def test_select_set_exc( with pytest.raises(HomeAssistantError, match=error): await hass.services.async_call( - SELECT_PLATFORM, + SELECT_DOMAIN, SERVICE_SELECT_OPTION, { - ATTR_ENTITY_ID: f"{SELECT_PLATFORM}.test_name_enum_203", + ATTR_ENTITY_ID: f"{SELECT_DOMAIN}.test_name_enum_203", ATTR_OPTION: "option 2", }, blocking=True, @@ -250,10 +250,10 @@ async def test_select_set_reauth_error( mock_rpc_device.enum_set.side_effect = InvalidAuthError await hass.services.async_call( - SELECT_PLATFORM, + SELECT_DOMAIN, SERVICE_SELECT_OPTION, { - ATTR_ENTITY_ID: f"{SELECT_PLATFORM}.test_name_enum_203", + ATTR_ENTITY_ID: f"{SELECT_DOMAIN}.test_name_enum_203", ATTR_OPTION: "option 2", }, blocking=True, @@ -280,7 +280,7 @@ async def test_rpc_cury_mode_select( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test Cury Mode select entity.""" - entity_id = f"{SELECT_PLATFORM}.test_name_mode" + entity_id = f"{SELECT_DOMAIN}.test_name_mode" status = {"cury:0": {"id": 0, "mode": "hall"}} monkeypatch.setattr(mock_rpc_device, "status", status) await init_integration(hass, 3) @@ -310,7 +310,7 @@ async def test_rpc_cury_mode_select( monkeypatch.setitem(mock_rpc_device.status["cury:0"], "mode", "reception") await hass.services.async_call( - SELECT_PLATFORM, + SELECT_DOMAIN, SERVICE_SELECT_OPTION, {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "reception"}, blocking=True, diff --git a/tests/components/shelly/test_text.py b/tests/components/shelly/test_text.py index ad8497a1d038ca..0dd8111ed31622 100644 --- a/tests/components/shelly/test_text.py +++ b/tests/components/shelly/test_text.py @@ -9,7 +9,7 @@ from homeassistant.components.shelly.const import DOMAIN from homeassistant.components.text import ( ATTR_VALUE, - DOMAIN as TEXT_PLATFORM, + DOMAIN as TEXT_DOMAIN, SERVICE_SET_VALUE, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState @@ -72,7 +72,7 @@ async def test_rpc_device_virtual_text( monkeypatch.setitem(mock_rpc_device.status["text:203"], "value", "sed do eiusmod") await hass.services.async_call( - TEXT_PLATFORM, + TEXT_DOMAIN, SERVICE_SET_VALUE, {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: "sed do eiusmod"}, blocking=True, @@ -105,7 +105,7 @@ async def test_rpc_remove_virtual_text_when_mode_label( device_entry = register_device(device_registry, config_entry) entity_id = register_entity( hass, - TEXT_PLATFORM, + TEXT_DOMAIN, "test_name_text_200", "text:200-text_generic", config_entry, @@ -129,7 +129,7 @@ async def test_rpc_remove_virtual_text_when_orphaned( device_entry = register_device(device_registry, config_entry) entity_id = register_entity( hass, - TEXT_PLATFORM, + TEXT_DOMAIN, "test_name_text_200", "text:200-text_generic", config_entry, @@ -180,10 +180,10 @@ async def test_text_set_exc( with pytest.raises(HomeAssistantError, match=error): await hass.services.async_call( - TEXT_PLATFORM, + TEXT_DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: f"{TEXT_PLATFORM}.test_name_text_203", + ATTR_ENTITY_ID: f"{TEXT_DOMAIN}.test_name_text_203", ATTR_VALUE: "new value", }, blocking=True, @@ -212,10 +212,10 @@ async def test_text_set_reauth_error( mock_rpc_device.text_set.side_effect = InvalidAuthError await hass.services.async_call( - TEXT_PLATFORM, + TEXT_DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: f"{TEXT_PLATFORM}.test_name_text_203", + ATTR_ENTITY_ID: f"{TEXT_DOMAIN}.test_name_text_203", ATTR_VALUE: "new value", }, blocking=True, diff --git a/tests/components/tractive/test_init.py b/tests/components/tractive/test_init.py index 9b84168eec5341..71934600c01a47 100644 --- a/tests/components/tractive/test_init.py +++ b/tests/components/tractive/test_init.py @@ -6,7 +6,7 @@ from aiotractive.exceptions import TractiveError, UnauthorizedError import pytest -from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.tractive.const import ( ATTR_DAILY_GOAL, ATTR_MINUTES_ACTIVE, @@ -233,7 +233,7 @@ async def test_remove_unsupported_sensor_entity( mock_config_entry.add_to_hass(hass) entity_registry.async_get_or_create( - SENSOR_PLATFORM, + SENSOR_DOMAIN, DOMAIN, f"pet_id_123_{sensor}", suggested_object_id=entity_id.rsplit(".", maxsplit=1)[-1], diff --git a/tests/components/weheat/conftest.py b/tests/components/weheat/conftest.py index 692792955fc5d3..8d2f70ea4726e7 100644 --- a/tests/components/weheat/conftest.py +++ b/tests/components/weheat/conftest.py @@ -9,7 +9,7 @@ from weheat.abstractions.heat_pump import HeatPump from homeassistant.components.application_credentials import ( - DOMAIN as APPLICATION_CREDENTIALS, + DOMAIN as APPLICATION_CREDENTIALS_DOMAIN, ClientCredential, async_import_client_credential, ) @@ -32,7 +32,7 @@ @pytest.fixture(autouse=True) async def setup_credentials(hass: HomeAssistant) -> None: """Fixture to setup credentials.""" - assert await async_setup_component(hass, APPLICATION_CREDENTIALS, {}) + assert await async_setup_component(hass, APPLICATION_CREDENTIALS_DOMAIN, {}) await async_import_client_credential( hass, DOMAIN, From c114ea266626ba3e99a4848ec48535aa7d6323ac Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 17 Feb 2026 12:31:31 +0100 Subject: [PATCH 14/36] Fix warning in Fritz switch tests (#163256) --- tests/components/fritz/test_switch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/components/fritz/test_switch.py b/tests/components/fritz/test_switch.py index 045369099a5270..0e990ed9c39996 100644 --- a/tests/components/fritz/test_switch.py +++ b/tests/components/fritz/test_switch.py @@ -290,6 +290,7 @@ async def test_switch_no_profile_entities_list( hass: HomeAssistant, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test Fritz!Tools switches with no profile entities.""" @@ -312,6 +313,7 @@ async def test_switch_no_mesh_wifi_uplink( hass: HomeAssistant, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test Fritz!Tools switches when no mesh WiFi uplink.""" @@ -330,6 +332,7 @@ async def test_switch_device_no_wan_access( hass: HomeAssistant, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test Fritz!Tools switches when device has no WAN access.""" @@ -355,6 +358,7 @@ async def test_switch_device_no_ip_address( hass: HomeAssistant, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test Fritz!Tools switches when device has no IP address.""" @@ -398,6 +402,7 @@ async def test_switch_turn_on_off( hass: HomeAssistant, fc_class_mock, fh_class_mock, + fs_class_mock, entity_id: str, wrapper_method: str, state_value: str, From ba695b5bd94d4b95eb9db7de4b2af9c9fe5f3c2f Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 17 Feb 2026 21:46:46 +1000 Subject: [PATCH 15/36] Add quality scale to Splunk (#162893) --- .../components/splunk/quality_scale.yaml | 139 ++++++++++++++++++ script/hassfest/quality_scale.py | 1 - 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/splunk/quality_scale.yaml diff --git a/homeassistant/components/splunk/quality_scale.yaml b/homeassistant/components/splunk/quality_scale.yaml new file mode 100644 index 00000000000000..fd3c6affb58038 --- /dev/null +++ b/homeassistant/components/splunk/quality_scale.yaml @@ -0,0 +1,139 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: | + Integration does not provide custom actions. + appropriate-polling: + status: exempt + comment: | + Event-driven push integration that listens to state changes, no polling occurs. + brands: done + common-modules: done + config-entry-unloading: done + config-flow-test-coverage: done + config-flow: + status: todo + comment: | + Missing `data_description` for `token` in `config.step.reauth_confirm` in strings.json. Tests fail with: "Translation not found for splunk: config.step.reauth_confirm.data_description.token". + dependency-transparency: + status: todo + comment: | + The hass-splunk library needs a public CI/CD pipeline. Add GitHub Actions workflow to https://github.com/Bre77/hass_splunk to automate lint, test, build, and publish to PyPI. + docs-actions: + status: exempt + comment: | + Integration does not provide custom actions. + docs-high-level-description: + status: todo + comment: | + Verify integration docs at https://www.home-assistant.io/integrations/splunk/ include a high-level description of Splunk with a link to https://www.splunk.com/ and explain the integration's purpose for users unfamiliar with Splunk. + docs-installation-instructions: + status: todo + comment: | + Verify integration docs include clear prerequisites and step-by-step setup instructions including how to configure Splunk HTTP Event Collector and obtain the required token. + docs-removal-instructions: + status: todo + comment: | + Verify integration docs include instructions on how to remove the integration and clarify what happens to data already in Splunk. + entity-event-setup: + status: exempt + comment: | + Integration does not create entities. + entity-unique-id: + status: exempt + comment: | + Integration does not create entities. + has-entity-name: + status: exempt + comment: | + Integration does not create entities. + integration-owner: done + reauthentication-flow: done + runtime-data: + status: todo + comment: | + Replace hass.data[DATA_FILTER] storage with entry.runtime_data. Create typed ConfigEntry in const.py as 'type SplunkConfigEntry = ConfigEntry[EntityFilter]', update async_setup_entry signature to use SplunkConfigEntry, replace hass.data[DATA_FILTER] assignments with entry.runtime_data, and update all references including line 236 in __init__.py. + test-before-configure: done + test-before-setup: done + test-coverage: done + unique-config-entry: done + + # Silver + action-exceptions: + status: exempt + comment: | + Integration does not provide custom actions. + docs-configuration-parameters: + status: exempt + comment: | + Integration does not have an options flow. + docs-installation-parameters: + status: todo + comment: | + Verify docs describe all config flow parameters including host, port, token, SSL settings, and entity filter. The strings.json has good data_description fields that should be reflected in documentation. + entity-unavailable: + status: exempt + comment: | + Integration does not create entities. + log-when-unavailable: + status: exempt + comment: | + Integration does not create entities. + parallel-updates: + status: exempt + comment: | + Integration does not create entities. + + # Gold + diagnostics: + status: todo + comment: | + Consider adding diagnostics support including config entry data with redacted token, connection status, event submission statistics, entity filter configuration, and recent error messages to help troubleshooting. + discovery: + status: exempt + comment: | + Integration does not support automatic discovery. + devices: + status: exempt + comment: | + Integration does not create devices. + entity-category: + status: exempt + comment: | + Integration does not create entities. + entity-device-class: + status: exempt + comment: | + Integration does not create entities. + entity-disabled-by-default: + status: exempt + comment: | + Integration does not create entities. + entity-translations: + status: exempt + comment: | + Integration does not create entities. + exception-translations: + status: todo + comment: | + Consider adding exception translations for user-facing errors beyond the current strings.json error section to provide more detailed translated error messages. + icon-translations: + status: exempt + comment: | + Integration does not create entities. + reconfiguration-flow: + status: todo + comment: | + Consider adding reconfiguration flow to allow users to update host, port, entity filter, and SSL settings without deleting and re-adding the config entry. + + # Platinum + async-dependency: + status: todo + comment: | + Verify all methods in hass-splunk library at https://github.com/Bre77/hass_splunk are truly async with no blocking calls or synchronous I/O operations. + inject-websession: done + strict-typing: + status: todo + comment: | + Add py.typed marker to hass-splunk library, add comprehensive type hints to all functions and methods in both library and integration, use custom typed ConfigEntry, add integration to homeassistant/components/.strict-typing file, and verify mypy passes. This should be done after runtime-data is implemented. diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index d2bf1422e5ab7d..1090de74dcadcd 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -894,7 +894,6 @@ class Rule: "spc", "speedtestdotnet", "spider", - "splunk", "spotify", "sql", "srp_energy", From 0337988be8e3e983839451fbefc24d8fedf8f3dd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:03:17 +0100 Subject: [PATCH 16/36] Improve type hints in meteoclimatic weather (#163244) --- .../components/meteoclimatic/weather.py | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index ba74cfeca5e288..3f814928026667 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -1,5 +1,7 @@ """Support for Meteoclimatic weather service.""" +from typing import TYPE_CHECKING + from meteoclimatic import Condition from homeassistant.components.weather import WeatherEntity @@ -47,56 +49,49 @@ class MeteoclimaticWeather( def __init__(self, coordinator: MeteoclimaticUpdateCoordinator) -> None: """Initialise the weather platform.""" super().__init__(coordinator) - self._unique_id = self.coordinator.data["station"].code - self._name = self.coordinator.data["station"].name - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Return the unique id of the sensor.""" - return self._unique_id + self._attr_unique_id = self.coordinator.data["station"].code + self._attr_name = self.coordinator.data["station"].name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" + unique_id = self.coordinator.config_entry.unique_id + if TYPE_CHECKING: + assert unique_id is not None return DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, + identifiers={(DOMAIN, unique_id)}, manufacturer=MANUFACTURER, model=MODEL, name=self.coordinator.name, ) @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" return format_condition(self.coordinator.data["weather"].condition) @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the temperature.""" return self.coordinator.data["weather"].temp_current @property - def humidity(self): + def humidity(self) -> float | None: """Return the humidity.""" return self.coordinator.data["weather"].humidity_current @property - def native_pressure(self): + def native_pressure(self) -> float | None: """Return the pressure.""" return self.coordinator.data["weather"].pressure_current @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the wind speed.""" return self.coordinator.data["weather"].wind_current @property - def wind_bearing(self): + def wind_bearing(self) -> float | None: """Return the wind bearing.""" return self.coordinator.data["weather"].wind_bearing From fdad9873e4ffb832adabd38e0dbc798f29383373 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:23:53 +0100 Subject: [PATCH 17/36] Mark weather method type hints as mandatory (#163247) --- pylint/plugins/hass_enforce_type_hints.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 278023c4f3cd1d..1c422c6cc452d5 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -3042,62 +3042,82 @@ class ClassTypeHintMatch: TypeHintMatch( function_name="native_temperature", return_type=["float", None], + mandatory=True, ), TypeHintMatch( function_name="native_temperature_unit", return_type=["str", None], + mandatory=True, ), TypeHintMatch( function_name="native_pressure", return_type=["float", None], + mandatory=True, ), TypeHintMatch( function_name="native_pressure_unit", return_type=["str", None], + mandatory=True, ), TypeHintMatch( function_name="humidity", return_type=["float", None], + mandatory=True, + ), + TypeHintMatch( + function_name="native_wind_gust_speed", + return_type=["float", None], + mandatory=True, ), TypeHintMatch( function_name="native_wind_speed", return_type=["float", None], + mandatory=True, ), TypeHintMatch( function_name="native_wind_speed_unit", return_type=["str", None], + mandatory=True, ), TypeHintMatch( function_name="wind_bearing", return_type=["float", "str", None], + mandatory=True, ), TypeHintMatch( function_name="ozone", return_type=["float", None], + mandatory=True, ), TypeHintMatch( function_name="native_visibility", return_type=["float", None], + mandatory=True, ), TypeHintMatch( function_name="native_visibility_unit", return_type=["str", None], + mandatory=True, ), TypeHintMatch( function_name="forecast", return_type=["list[Forecast]", None], + mandatory=True, ), TypeHintMatch( function_name="native_precipitation_unit", return_type=["str", None], + mandatory=True, ), TypeHintMatch( function_name="precision", return_type="float", + mandatory=True, ), TypeHintMatch( function_name="condition", return_type=["str", None], + mandatory=True, ), ], ), From 58e4a42a1b12160a8e86a4f7b948c9784f071d91 Mon Sep 17 00:00:00 2001 From: Tom Matheussen <13683094+Tommatheussen@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:55:00 +0100 Subject: [PATCH 18/36] Add coordinator for Satel Integra (#158533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Abílio Costa --- .../components/satel_integra/__init__.py | 110 ++++------------- .../satel_integra/alarm_control_panel.py | 52 +++----- .../components/satel_integra/binary_sensor.py | 59 ++++----- .../components/satel_integra/client.py | 105 ++++++++++++++++ .../components/satel_integra/config_flow.py | 2 +- .../components/satel_integra/const.py | 12 -- .../components/satel_integra/coordinator.py | 114 ++++++++++++++++++ .../components/satel_integra/entity.py | 15 ++- .../components/satel_integra/switch.py | 54 ++++----- tests/components/satel_integra/__init__.py | 21 ++++ tests/components/satel_integra/conftest.py | 12 +- .../satel_integra/test_alarm_control_panel.py | 7 +- .../satel_integra/test_binary_sensor.py | 57 ++++++--- tests/components/satel_integra/test_switch.py | 50 ++++++-- 14 files changed, 429 insertions(+), 241 deletions(-) create mode 100644 homeassistant/components/satel_integra/client.py create mode 100644 homeassistant/components/satel_integra/coordinator.py diff --git a/homeassistant/components/satel_integra/__init__.py b/homeassistant/components/satel_integra/__init__.py index c2fcb6fe62c4f1..13547cf84db808 100644 --- a/homeassistant/components/satel_integra/__init__.py +++ b/homeassistant/components/satel_integra/__init__.py @@ -2,30 +2,21 @@ import logging -from satel_integra.satel_integra import AsyncSatel import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - CONF_CODE, - CONF_HOST, - CONF_NAME, - CONF_PORT, - EVENT_HOMEASSISTANT_STOP, - Platform, -) +from homeassistant.const import CONF_CODE, CONF_HOST, CONF_NAME, CONF_PORT, Platform from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( config_validation as cv, device_registry as dr, issue_registry as ir, ) -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.typing import ConfigType +from .client import SatelClient from .const import ( CONF_ARM_HOME_MODE, CONF_DEVICE_PARTITIONS, @@ -41,15 +32,17 @@ DEFAULT_PORT, DEFAULT_ZONE_TYPE, DOMAIN, - SIGNAL_OUTPUTS_UPDATED, - SIGNAL_PANEL_MESSAGE, - SIGNAL_ZONES_UPDATED, SUBENTRY_TYPE_OUTPUT, SUBENTRY_TYPE_PARTITION, SUBENTRY_TYPE_SWITCHABLE_OUTPUT, SUBENTRY_TYPE_ZONE, - ZONES, +) +from .coordinator import ( SatelConfigEntry, + SatelIntegraData, + SatelIntegraOutputsCoordinator, + SatelIntegraPartitionsCoordinator, + SatelIntegraZonesCoordinator, ) _LOGGER = logging.getLogger(__name__) @@ -159,51 +152,25 @@ async def _async_import(hass: HomeAssistant, config: ConfigType) -> None: async def async_setup_entry(hass: HomeAssistant, entry: SatelConfigEntry) -> bool: """Set up Satel Integra from a config entry.""" - host = entry.data[CONF_HOST] - port = entry.data[CONF_PORT] - - # Make sure we initialize the Satel controller with the configured entries to monitor - partitions = [ - subentry.data[CONF_PARTITION_NUMBER] - for subentry in entry.subentries.values() - if subentry.subentry_type == SUBENTRY_TYPE_PARTITION - ] - - zones = [ - subentry.data[CONF_ZONE_NUMBER] - for subentry in entry.subentries.values() - if subentry.subentry_type == SUBENTRY_TYPE_ZONE - ] - - outputs = [ - subentry.data[CONF_OUTPUT_NUMBER] - for subentry in entry.subentries.values() - if subentry.subentry_type == SUBENTRY_TYPE_OUTPUT - ] - - switchable_outputs = [ - subentry.data[CONF_SWITCHABLE_OUTPUT_NUMBER] - for subentry in entry.subentries.values() - if subentry.subentry_type == SUBENTRY_TYPE_SWITCHABLE_OUTPUT - ] - - monitored_outputs = outputs + switchable_outputs - - controller = AsyncSatel(host, port, hass.loop, zones, monitored_outputs, partitions) + client = SatelClient(hass, entry) - result = await controller.connect() + coordinator_zones = SatelIntegraZonesCoordinator(hass, entry, client) + coordinator_outputs = SatelIntegraOutputsCoordinator(hass, entry, client) + coordinator_partitions = SatelIntegraPartitionsCoordinator(hass, entry, client) - if not result: - raise ConfigEntryNotReady("Controller failed to connect") - - entry.runtime_data = controller - - @callback - def _close(*_): - controller.close() + await client.async_connect( + coordinator_zones.zones_update_callback, + coordinator_outputs.outputs_update_callback, + coordinator_partitions.partitions_update_callback, + ) + entry.runtime_data = SatelIntegraData( + client=client, + coordinator_zones=coordinator_zones, + coordinator_outputs=coordinator_outputs, + coordinator_partitions=coordinator_partitions, + ) entry.async_on_unload(entry.add_update_listener(update_listener)) - entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close)) device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -214,33 +181,6 @@ def _close(*_): await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - @callback - def alarm_status_update_callback(): - """Send status update received from alarm to Home Assistant.""" - _LOGGER.debug("Sending request to update panel state") - async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE) - - @callback - def zones_update_callback(status): - """Update zone objects as per notification from the alarm.""" - _LOGGER.debug("Zones callback, status: %s", status) - async_dispatcher_send(hass, SIGNAL_ZONES_UPDATED, status[ZONES]) - - @callback - def outputs_update_callback(status): - """Update zone objects as per notification from the alarm.""" - _LOGGER.debug("Outputs updated callback , status: %s", status) - async_dispatcher_send(hass, SIGNAL_OUTPUTS_UPDATED, status["outputs"]) - - # Create a task instead of adding a tracking job, since this task will - # run until the connection to satel_integra is closed. - hass.loop.create_task(controller.keep_alive()) - hass.loop.create_task( - controller.monitor_status( - alarm_status_update_callback, zones_update_callback, outputs_update_callback - ) - ) - return True @@ -248,8 +188,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: SatelConfigEntry) -> bo """Unloading the Satel platforms.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - controller = entry.runtime_data - controller.close() + runtime_data = entry.runtime_data + runtime_data.client.close() return unload_ok diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index d17c7d995b4d23..ed72698cb3d41e 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -5,7 +5,7 @@ import asyncio import logging -from satel_integra.satel_integra import AlarmState, AsyncSatel +from satel_integra.satel_integra import AlarmState from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, @@ -15,16 +15,10 @@ ) from homeassistant.config_entries import ConfigSubentry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import ( - CONF_ARM_HOME_MODE, - CONF_PARTITION_NUMBER, - SIGNAL_PANEL_MESSAGE, - SUBENTRY_TYPE_PARTITION, - SatelConfigEntry, -) +from .const import CONF_ARM_HOME_MODE, CONF_PARTITION_NUMBER, SUBENTRY_TYPE_PARTITION +from .coordinator import SatelConfigEntry, SatelIntegraPartitionsCoordinator from .entity import SatelIntegraEntity ALARM_STATE_MAP = { @@ -49,7 +43,7 @@ async def async_setup_entry( ) -> None: """Set up for Satel Integra alarm panels.""" - controller = config_entry.runtime_data + runtime_data = config_entry.runtime_data partition_subentries = filter( lambda entry: entry.subentry_type == SUBENTRY_TYPE_PARTITION, @@ -63,7 +57,7 @@ async def async_setup_entry( async_add_entities( [ SatelIntegraAlarmPanel( - controller, + runtime_data.coordinator_partitions, config_entry.entry_id, subentry, partition_num, @@ -74,8 +68,10 @@ async def async_setup_entry( ) -class SatelIntegraAlarmPanel(SatelIntegraEntity, AlarmControlPanelEntity): - """Representation of an AlarmDecoder-based alarm panel.""" +class SatelIntegraAlarmPanel( + SatelIntegraEntity[SatelIntegraPartitionsCoordinator], AlarmControlPanelEntity +): + """Representation of a Satel Integra-based alarm panel.""" _attr_code_format = CodeFormat.NUMBER _attr_supported_features = ( @@ -85,7 +81,7 @@ class SatelIntegraAlarmPanel(SatelIntegraEntity, AlarmControlPanelEntity): def __init__( self, - controller: AsyncSatel, + coordinator: SatelIntegraPartitionsCoordinator, config_entry_id: str, subentry: ConfigSubentry, device_number: int, @@ -93,7 +89,7 @@ def __init__( ) -> None: """Initialize the alarm panel.""" super().__init__( - controller, + coordinator, config_entry_id, subentry, device_number, @@ -101,19 +97,11 @@ def __init__( self._arm_home_mode = arm_home_mode - async def async_added_to_hass(self) -> None: - """Update alarm status and register callbacks for future updates.""" self._attr_alarm_state = self._read_alarm_state() - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_PANEL_MESSAGE, self._update_alarm_status - ) - ) - @callback - def _update_alarm_status(self) -> None: - """Handle alarm status update.""" + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" state = self._read_alarm_state() if state != self._attr_alarm_state: @@ -123,14 +111,14 @@ def _update_alarm_status(self) -> None: def _read_alarm_state(self) -> AlarmControlPanelState | None: """Read current status of the alarm and translate it into HA status.""" - if not self._satel.connected: + if not self._controller.connected: _LOGGER.debug("Alarm panel not connected") return None for satel_state, ha_state in ALARM_STATE_MAP.items(): if ( - satel_state in self._satel.partition_states - and self._device_number in self._satel.partition_states[satel_state] + satel_state in self.coordinator.data + and self._device_number in self.coordinator.data[satel_state] ): return ha_state @@ -146,21 +134,21 @@ async def async_alarm_disarm(self, code: str | None = None) -> None: self._attr_alarm_state == AlarmControlPanelState.TRIGGERED ) - await self._satel.disarm(code, [self._device_number]) + await self._controller.disarm(code, [self._device_number]) if clear_alarm_necessary: # Wait 1s before clearing the alarm await asyncio.sleep(1) - await self._satel.clear_alarm(code, [self._device_number]) + await self._controller.clear_alarm(code, [self._device_number]) async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if code: - await self._satel.arm(code, [self._device_number]) + await self._controller.arm(code, [self._device_number]) async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if code: - await self._satel.arm(code, [self._device_number], self._arm_home_mode) + await self._controller.arm(code, [self._device_number], self._arm_home_mode) diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index 94e791532cf98e..a16fba0304691d 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -2,27 +2,22 @@ from __future__ import annotations -from satel_integra.satel_integra import AsyncSatel - from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigSubentry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( CONF_OUTPUT_NUMBER, CONF_ZONE_NUMBER, CONF_ZONE_TYPE, - SIGNAL_OUTPUTS_UPDATED, - SIGNAL_ZONES_UPDATED, SUBENTRY_TYPE_OUTPUT, SUBENTRY_TYPE_ZONE, - SatelConfigEntry, ) +from .coordinator import SatelConfigEntry, SatelIntegraBaseCoordinator from .entity import SatelIntegraEntity @@ -33,7 +28,7 @@ async def async_setup_entry( ) -> None: """Set up the Satel Integra binary sensor devices.""" - controller = config_entry.runtime_data + runtime_data = config_entry.runtime_data zone_subentries = filter( lambda entry: entry.subentry_type == SUBENTRY_TYPE_ZONE, @@ -47,12 +42,11 @@ async def async_setup_entry( async_add_entities( [ SatelIntegraBinarySensor( - controller, + runtime_data.coordinator_zones, config_entry.entry_id, subentry, zone_num, zone_type, - SIGNAL_ZONES_UPDATED, ) ], config_subentry_id=subentry.subentry_id, @@ -70,59 +64,50 @@ async def async_setup_entry( async_add_entities( [ SatelIntegraBinarySensor( - controller, + runtime_data.coordinator_outputs, config_entry.entry_id, subentry, output_num, ouput_type, - SIGNAL_OUTPUTS_UPDATED, ) ], config_subentry_id=subentry.subentry_id, ) -class SatelIntegraBinarySensor(SatelIntegraEntity, BinarySensorEntity): - """Representation of an Satel Integra binary sensor.""" +class SatelIntegraBinarySensor[_CoordinatorT: SatelIntegraBaseCoordinator]( + SatelIntegraEntity[_CoordinatorT], BinarySensorEntity +): + """Base binary sensor for Satel Integra.""" def __init__( self, - controller: AsyncSatel, + coordinator: _CoordinatorT, config_entry_id: str, subentry: ConfigSubentry, device_number: int, device_class: BinarySensorDeviceClass, - react_to_signal: str, ) -> None: """Initialize the binary_sensor.""" super().__init__( - controller, + coordinator, config_entry_id, subentry, device_number, ) self._attr_device_class = device_class - self._react_to_signal = react_to_signal - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - if self._react_to_signal == SIGNAL_OUTPUTS_UPDATED: - self._attr_is_on = self._device_number in self._satel.violated_outputs - else: - self._attr_is_on = self._device_number in self._satel.violated_zones - - self.async_on_remove( - async_dispatcher_connect( - self.hass, self._react_to_signal, self._devices_updated - ) - ) + + self._attr_is_on = self._get_state_from_coordinator() @callback - def _devices_updated(self, zones: dict[int, int]): - """Update the zone's state, if needed.""" - if self._device_number in zones: - new_state = zones[self._device_number] == 1 - if new_state != self._attr_is_on: - self._attr_is_on = new_state - self.async_write_ha_state() + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + new_state = self._get_state_from_coordinator() + if new_state != self._attr_is_on: + self._attr_is_on = new_state + self.async_write_ha_state() + + def _get_state_from_coordinator(self) -> bool | None: + """Method to get binary sensor state from coordinator data.""" + return self.coordinator.data.get(self._device_number) diff --git a/homeassistant/components/satel_integra/client.py b/homeassistant/components/satel_integra/client.py new file mode 100644 index 00000000000000..6950583f17306a --- /dev/null +++ b/homeassistant/components/satel_integra/client.py @@ -0,0 +1,105 @@ +"""Satel Integra client.""" + +from collections.abc import Callable + +from satel_integra.satel_integra import AsyncSatel + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import ( + CONF_OUTPUT_NUMBER, + CONF_PARTITION_NUMBER, + CONF_SWITCHABLE_OUTPUT_NUMBER, + CONF_ZONE_NUMBER, + SUBENTRY_TYPE_OUTPUT, + SUBENTRY_TYPE_PARTITION, + SUBENTRY_TYPE_SWITCHABLE_OUTPUT, + SUBENTRY_TYPE_ZONE, +) + + +class SatelClient: + """Client to connect to Satel Integra.""" + + controller: AsyncSatel + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the client wrapper.""" + self.hass = hass + self.config_entry = entry + + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + + # Make sure we initialize the Satel controller with the configured entries to monitor + partitions = [ + subentry.data[CONF_PARTITION_NUMBER] + for subentry in entry.subentries.values() + if subentry.subentry_type == SUBENTRY_TYPE_PARTITION + ] + + zones = [ + subentry.data[CONF_ZONE_NUMBER] + for subentry in entry.subentries.values() + if subentry.subentry_type == SUBENTRY_TYPE_ZONE + ] + + outputs = [ + subentry.data[CONF_OUTPUT_NUMBER] + for subentry in entry.subentries.values() + if subentry.subentry_type == SUBENTRY_TYPE_OUTPUT + ] + + switchable_outputs = [ + subentry.data[CONF_SWITCHABLE_OUTPUT_NUMBER] + for subentry in entry.subentries.values() + if subentry.subentry_type == SUBENTRY_TYPE_SWITCHABLE_OUTPUT + ] + + monitored_outputs = outputs + switchable_outputs + + self.controller = AsyncSatel( + host, port, hass.loop, zones, monitored_outputs, partitions + ) + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close) + ) + + async def async_connect( + self, + zones_update_callback: Callable[[dict[str, dict[int, int]]], None], + outputs_update_callback: Callable[[dict[str, dict[int, int]]], None], + partitions_update_callback: Callable[[], None], + ) -> None: + """Start controller connection.""" + result = await self.controller.connect() + if not result: + raise ConfigEntryNotReady("Controller failed to connect") + + self.config_entry.async_create_background_task( + self.hass, + self.controller.keep_alive(), + f"satel_integra.{self.config_entry.entry_id}.keep_alive", + eager_start=False, + ) + + self.config_entry.async_create_background_task( + self.hass, + self.controller.monitor_status( + partitions_update_callback, + zones_update_callback, + outputs_update_callback, + ), + f"satel_integra.{self.config_entry.entry_id}.monitor_status", + eager_start=False, + ) + + @callback + def close(self, *args, **kwargs) -> None: + """Close the connection.""" + + self.controller.close() diff --git a/homeassistant/components/satel_integra/config_flow.py b/homeassistant/components/satel_integra/config_flow.py index 1f015c622a9002..9e9463cf73079d 100644 --- a/homeassistant/components/satel_integra/config_flow.py +++ b/homeassistant/components/satel_integra/config_flow.py @@ -40,8 +40,8 @@ SUBENTRY_TYPE_PARTITION, SUBENTRY_TYPE_SWITCHABLE_OUTPUT, SUBENTRY_TYPE_ZONE, - SatelConfigEntry, ) +from .coordinator import SatelConfigEntry _LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/satel_integra/const.py b/homeassistant/components/satel_integra/const.py index 822fbe7594b23d..917a58e493cb78 100644 --- a/homeassistant/components/satel_integra/const.py +++ b/homeassistant/components/satel_integra/const.py @@ -1,9 +1,5 @@ """Constants for the Satel Integra integration.""" -from satel_integra.satel_integra import AsyncSatel - -from homeassistant.config_entries import ConfigEntry - DEFAULT_CONF_ARM_HOME_MODE = 1 DEFAULT_PORT = 7094 DEFAULT_ZONE_TYPE = "motion" @@ -28,11 +24,3 @@ CONF_SWITCHABLE_OUTPUTS = "switchable_outputs" ZONES = "zones" - - -SIGNAL_PANEL_MESSAGE = "satel_integra.panel_message" - -SIGNAL_ZONES_UPDATED = "satel_integra.zones_updated" -SIGNAL_OUTPUTS_UPDATED = "satel_integra.outputs_updated" - -type SatelConfigEntry = ConfigEntry[AsyncSatel] diff --git a/homeassistant/components/satel_integra/coordinator.py b/homeassistant/components/satel_integra/coordinator.py new file mode 100644 index 00000000000000..66bf3c7a3ee7b5 --- /dev/null +++ b/homeassistant/components/satel_integra/coordinator.py @@ -0,0 +1,114 @@ +"""Coordinator for Satel Integra.""" + +from __future__ import annotations + +from dataclasses import dataclass +import logging + +from satel_integra.satel_integra import AlarmState + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .client import SatelClient +from .const import ZONES + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class SatelIntegraData: + """Data for the satel_integra integration.""" + + client: SatelClient + coordinator_zones: SatelIntegraZonesCoordinator + coordinator_outputs: SatelIntegraOutputsCoordinator + coordinator_partitions: SatelIntegraPartitionsCoordinator + + +type SatelConfigEntry = ConfigEntry[SatelIntegraData] + + +class SatelIntegraBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]): + """DataUpdateCoordinator base class for Satel Integra.""" + + config_entry: SatelConfigEntry + + def __init__( + self, hass: HomeAssistant, entry: SatelConfigEntry, client: SatelClient + ) -> None: + """Initialize the base coordinator.""" + self.client = client + + super().__init__( + hass, + _LOGGER, + config_entry=entry, + name=f"{entry.entry_id} {self.__class__.__name__}", + ) + + +class SatelIntegraZonesCoordinator(SatelIntegraBaseCoordinator[dict[int, bool]]): + """DataUpdateCoordinator to handle zone updates.""" + + def __init__( + self, hass: HomeAssistant, entry: SatelConfigEntry, client: SatelClient + ) -> None: + """Initialize the coordinator.""" + super().__init__(hass, entry, client) + + self.data = {} + + @callback + def zones_update_callback(self, status: dict[str, dict[int, int]]) -> None: + """Update zone objects as per notification from the alarm.""" + _LOGGER.debug("Zones callback, status: %s", status) + + update_data = {zone: value == 1 for zone, value in status[ZONES].items()} + + self.async_set_updated_data(update_data) + + +class SatelIntegraOutputsCoordinator(SatelIntegraBaseCoordinator[dict[int, bool]]): + """DataUpdateCoordinator to handle output updates.""" + + def __init__( + self, hass: HomeAssistant, entry: SatelConfigEntry, client: SatelClient + ) -> None: + """Initialize the coordinator.""" + super().__init__(hass, entry, client) + + self.data = {} + + @callback + def outputs_update_callback(self, status: dict[str, dict[int, int]]) -> None: + """Update output objects as per notification from the alarm.""" + _LOGGER.debug("Outputs callback, status: %s", status) + + update_data = { + output: value == 1 for output, value in status["outputs"].items() + } + + self.async_set_updated_data(update_data) + + +class SatelIntegraPartitionsCoordinator( + SatelIntegraBaseCoordinator[dict[AlarmState, list[int]]] +): + """DataUpdateCoordinator to handle partition state updates.""" + + def __init__( + self, hass: HomeAssistant, entry: SatelConfigEntry, client: SatelClient + ) -> None: + """Initialize the coordinator.""" + super().__init__(hass, entry, client) + + self.data = {} + + @callback + def partitions_update_callback(self) -> None: + """Update partition objects as per notification from the alarm.""" + _LOGGER.debug("Sending request to update panel state") + + self.async_set_updated_data(self.client.controller.partition_states) diff --git a/homeassistant/components/satel_integra/entity.py b/homeassistant/components/satel_integra/entity.py index 0d18e6348921a1..a37339147189d0 100644 --- a/homeassistant/components/satel_integra/entity.py +++ b/homeassistant/components/satel_integra/entity.py @@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigSubentry from homeassistant.const import CONF_NAME from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( DOMAIN, @@ -18,6 +18,7 @@ SUBENTRY_TYPE_SWITCHABLE_OUTPUT, SUBENTRY_TYPE_ZONE, ) +from .coordinator import SatelIntegraBaseCoordinator SubentryTypeToEntityType: dict[str, str] = { SUBENTRY_TYPE_PARTITION: "alarm_panel", @@ -27,23 +28,29 @@ } -class SatelIntegraEntity(Entity): +class SatelIntegraEntity[_CoordinatorT: SatelIntegraBaseCoordinator]( + CoordinatorEntity[_CoordinatorT] +): """Defines a base Satel Integra entity.""" _attr_should_poll = False _attr_has_entity_name = True _attr_name = None + _controller: AsyncSatel + def __init__( self, - controller: AsyncSatel, + coordinator: _CoordinatorT, config_entry_id: str, subentry: ConfigSubentry, device_number: int, ) -> None: """Initialize the Satel Integra entity.""" + super().__init__(coordinator) + + self._controller = coordinator.client.controller - self._satel = controller self._device_number = device_number entity_type = SubentryTypeToEntityType[subentry.subentry_type] diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index 4ae84e3312e26b..7b321d6eeda2cb 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -4,21 +4,14 @@ from typing import Any -from satel_integra.satel_integra import AsyncSatel - from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigSubentry from homeassistant.const import CONF_CODE from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import ( - CONF_SWITCHABLE_OUTPUT_NUMBER, - SIGNAL_OUTPUTS_UPDATED, - SUBENTRY_TYPE_SWITCHABLE_OUTPUT, - SatelConfigEntry, -) +from .const import CONF_SWITCHABLE_OUTPUT_NUMBER, SUBENTRY_TYPE_SWITCHABLE_OUTPUT +from .coordinator import SatelConfigEntry, SatelIntegraOutputsCoordinator from .entity import SatelIntegraEntity @@ -29,7 +22,7 @@ async def async_setup_entry( ) -> None: """Set up the Satel Integra switch devices.""" - controller = config_entry.runtime_data + runtime_data = config_entry.runtime_data switchable_output_subentries = filter( lambda entry: entry.subentry_type == SUBENTRY_TYPE_SWITCHABLE_OUTPUT, @@ -42,7 +35,7 @@ async def async_setup_entry( async_add_entities( [ SatelIntegraSwitch( - controller, + runtime_data.coordinator_outputs, config_entry.entry_id, subentry, switchable_output_num, @@ -53,12 +46,14 @@ async def async_setup_entry( ) -class SatelIntegraSwitch(SatelIntegraEntity, SwitchEntity): +class SatelIntegraSwitch( + SatelIntegraEntity[SatelIntegraOutputsCoordinator], SwitchEntity +): """Representation of an Satel Integra switch.""" def __init__( self, - controller: AsyncSatel, + coordinator: SatelIntegraOutputsCoordinator, config_entry_id: str, subentry: ConfigSubentry, device_number: int, @@ -66,7 +61,7 @@ def __init__( ) -> None: """Initialize the switch.""" super().__init__( - controller, + coordinator, config_entry_id, subentry, device_number, @@ -74,33 +69,28 @@ def __init__( self._code = code - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - self._attr_is_on = self._device_number in self._satel.violated_outputs - - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_OUTPUTS_UPDATED, self._devices_updated - ) - ) + self._attr_is_on = self._get_state_from_coordinator() @callback - def _devices_updated(self, outputs: dict[int, int]) -> None: - """Update switch state, if needed.""" - if self._device_number in outputs: - new_state = outputs[self._device_number] == 1 - if new_state != self._attr_is_on: - self._attr_is_on = new_state - self.async_write_ha_state() + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + new_state = self._get_state_from_coordinator() + if new_state != self._attr_is_on: + self._attr_is_on = new_state + self.async_write_ha_state() + + def _get_state_from_coordinator(self) -> bool | None: + """Method to get switch state from coordinator data.""" + return self.coordinator.data.get(self._device_number) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - await self._satel.set_output(self._code, self._device_number, True) + await self._controller.set_output(self._code, self._device_number, True) self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - await self._satel.set_output(self._code, self._device_number, False) + await self._controller.set_output(self._code, self._device_number, False) self._attr_is_on = False self.async_write_ha_state() diff --git a/tests/components/satel_integra/__init__.py b/tests/components/satel_integra/__init__.py index 9d33a292c3b548..6d9a4474693b07 100644 --- a/tests/components/satel_integra/__init__.py +++ b/tests/components/satel_integra/__init__.py @@ -1,5 +1,10 @@ """The tests for Satel Integra integration.""" +from collections.abc import Callable +from unittest.mock import AsyncMock + +import pytest + from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.satel_integra import ( CONF_ARM_HOME_MODE, @@ -80,3 +85,19 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + + +def get_monitor_callbacks( + mock_satel: AsyncMock, +) -> tuple[ + Callable[[], None], + Callable[[dict[str, dict[int, int]]], None], + Callable[[dict[str, dict[int, int]]], None], +]: + """Return (partitions_cb, zones_cb, outputs_cb) passed to monitor_status.""" + if not mock_satel.monitor_status.call_args_list: + pytest.fail("monitor_status was not called") + + call = mock_satel.monitor_status.call_args_list[-1] + partitions_cb, zones_cb, outputs_cb = call.args + return partitions_cb, zones_cb, outputs_cb diff --git a/tests/components/satel_integra/conftest.py b/tests/components/satel_integra/conftest.py index 30cddc580c297e..1409dacd4776be 100644 --- a/tests/components/satel_integra/conftest.py +++ b/tests/components/satel_integra/conftest.py @@ -36,7 +36,7 @@ def mock_satel() -> Generator[AsyncMock]: """Override the satel test.""" with ( patch( - "homeassistant.components.satel_integra.AsyncSatel", + "homeassistant.components.satel_integra.client.AsyncSatel", autospec=True, ) as mock_client, patch( @@ -45,12 +45,22 @@ def mock_satel() -> Generator[AsyncMock]: ), ): client = mock_client.return_value + client.partition_states = {} client.violated_outputs = [] client.violated_zones = [] + client.connect = AsyncMock(return_value=True) client.set_output = AsyncMock() + # Immediately push baseline values so entities have stable states for snapshots + async def _monitor_status(partitions_cb, zones_cb, outputs_cb): + partitions_cb() + zones_cb({"zones": {1: 0}}) + outputs_cb({"outputs": {1: 0}}) + + client.monitor_status = AsyncMock(side_effect=_monitor_status) + yield client diff --git a/tests/components/satel_integra/test_alarm_control_panel.py b/tests/components/satel_integra/test_alarm_control_panel.py index 36c3ff55787785..f447739d30e439 100644 --- a/tests/components/satel_integra/test_alarm_control_panel.py +++ b/tests/components/satel_integra/test_alarm_control_panel.py @@ -24,7 +24,7 @@ from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry -from . import MOCK_CODE, MOCK_ENTRY_ID, setup_integration +from . import MOCK_CODE, MOCK_ENTRY_ID, get_monitor_callbacks, setup_integration from tests.common import MockConfigEntry, snapshot_platform @@ -59,7 +59,7 @@ async def test_alarm_control_panel( assert device_entry == snapshot(name="device") -async def test_alarm_control_panel_initial_state_on( +async def test_alarm_control_panel_initial_state( hass: HomeAssistant, mock_satel: AsyncMock, mock_config_entry_with_subentries: MockConfigEntry, @@ -104,8 +104,7 @@ async def test_alarm_status_callback( == AlarmControlPanelState.DISARMED ) - monitor_status_call = mock_satel.monitor_status.call_args_list[0][0] - alarm_panel_update_method = monitor_status_call[0] + alarm_panel_update_method, _, _ = get_monitor_callbacks(mock_satel) mock_satel.partition_states = {source_state: [1]} diff --git a/tests/components/satel_integra/test_binary_sensor.py b/tests/components/satel_integra/test_binary_sensor.py index be3227188c5bf0..7d125e53309dde 100644 --- a/tests/components/satel_integra/test_binary_sensor.py +++ b/tests/components/satel_integra/test_binary_sensor.py @@ -6,15 +6,14 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.binary_sensor import STATE_OFF, STATE_ON from homeassistant.components.satel_integra.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import Platform +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry -from . import setup_integration +from . import get_monitor_callbacks, setup_integration from tests.common import MockConfigEntry, snapshot_platform @@ -58,19 +57,36 @@ async def test_binary_sensors( assert device_entry == snapshot(name="device-output") -async def test_binary_sensor_initial_state_on( +@pytest.mark.parametrize( + ("violated_entries", "expected_state"), + [ + ({2: 1}, STATE_UNKNOWN), + ({1: 0}, STATE_OFF), + ({1: 1}, STATE_ON), + ], +) +async def test_binary_sensor_initial_state( hass: HomeAssistant, mock_satel: AsyncMock, mock_config_entry_with_subentries: MockConfigEntry, + violated_entries: dict[int, int], + expected_state: str, ) -> None: - """Test binary sensors have a correct initial state ON after initialization.""" - mock_satel.violated_zones = [1] - mock_satel.violated_outputs = [1] + """Test binary sensors have a correct initial state after initialization.""" + + # Instantly call callback to ensure we have initial data set + async def mock_monitor_callback( + alarm_status_callback, zones_callback, outputs_callback + ): + outputs_callback({"outputs": violated_entries}) + zones_callback({"zones": violated_entries}) + + mock_satel.monitor_status = AsyncMock(side_effect=mock_monitor_callback) await setup_integration(hass, mock_config_entry_with_subentries) - assert hass.states.get("binary_sensor.zone").state == STATE_ON - assert hass.states.get("binary_sensor.output").state == STATE_ON + assert hass.states.get("binary_sensor.zone").state == expected_state + assert hass.states.get("binary_sensor.output").state == expected_state async def test_binary_sensor_callback( @@ -84,19 +100,20 @@ async def test_binary_sensor_callback( assert hass.states.get("binary_sensor.zone").state == STATE_OFF assert hass.states.get("binary_sensor.output").state == STATE_OFF - monitor_status_call = mock_satel.monitor_status.call_args_list[0][0] - output_update_method = monitor_status_call[2] - zone_update_method = monitor_status_call[1] - - # Should do nothing, only react to it's own number - output_update_method({"outputs": {2: 1}}) - zone_update_method({"zones": {2: 1}}) - - assert hass.states.get("binary_sensor.zone").state == STATE_OFF - assert hass.states.get("binary_sensor.output").state == STATE_OFF + _, zone_update_method, output_update_method = get_monitor_callbacks(mock_satel) output_update_method({"outputs": {1: 1}}) zone_update_method({"zones": {1: 1}}) - assert hass.states.get("binary_sensor.zone").state == STATE_ON assert hass.states.get("binary_sensor.output").state == STATE_ON + + output_update_method({"outputs": {1: 0}}) + zone_update_method({"zones": {1: 0}}) + assert hass.states.get("binary_sensor.zone").state == STATE_OFF + assert hass.states.get("binary_sensor.output").state == STATE_OFF + + # The client library should always report all entries, but test that we set the status correctly if it doesn't + output_update_method({"outputs": {2: 1}}) + zone_update_method({"zones": {2: 1}}) + assert hass.states.get("binary_sensor.zone").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.output").state == STATE_UNKNOWN diff --git a/tests/components/satel_integra/test_switch.py b/tests/components/satel_integra/test_switch.py index acf8d57abaa67f..165324075592c6 100644 --- a/tests/components/satel_integra/test_switch.py +++ b/tests/components/satel_integra/test_switch.py @@ -6,19 +6,24 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.binary_sensor import STATE_OFF, STATE_ON from homeassistant.components.satel_integra.const import DOMAIN from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry -from . import MOCK_CODE, MOCK_ENTRY_ID, setup_integration +from . import MOCK_CODE, MOCK_ENTRY_ID, get_monitor_callbacks, setup_integration from tests.common import MockConfigEntry, snapshot_platform @@ -53,17 +58,34 @@ async def test_switches( assert device_entry == snapshot(name="device") -async def test_switch_initial_state_on( +@pytest.mark.parametrize( + ("violated_outputs", "expected_state"), + [ + ({2: 1}, STATE_UNKNOWN), + ({1: 0}, STATE_OFF), + ({1: 1}, STATE_ON), + ], +) +async def test_switch_initial_state( hass: HomeAssistant, mock_satel: AsyncMock, mock_config_entry_with_subentries: MockConfigEntry, + violated_outputs: dict[int, int], + expected_state: str, ) -> None: - """Test switch has a correct initial state ON after initialization.""" - mock_satel.violated_outputs = [1] + """Test switch has a correct initial state after initialization.""" + + # Instantly call callback to ensure we have initial data set + async def mock_monitor_callback( + alarm_status_callback, zones_callback, outputs_callback + ): + outputs_callback({"outputs": violated_outputs}) + + mock_satel.monitor_status = AsyncMock(side_effect=mock_monitor_callback) await setup_integration(hass, mock_config_entry_with_subentries) - assert hass.states.get("switch.switchable_output").state == STATE_ON + assert hass.states.get("switch.switchable_output").state == expected_state async def test_switch_callback( @@ -76,16 +98,18 @@ async def test_switch_callback( assert hass.states.get("switch.switchable_output").state == STATE_OFF - monitor_status_call = mock_satel.monitor_status.call_args_list[0][0] - output_update_method = monitor_status_call[2] - - # Should do nothing, only react to it's own number - output_update_method({"outputs": {2: 1}}) - assert hass.states.get("switch.switchable_output").state == STATE_OFF + _, _, output_update_method = get_monitor_callbacks(mock_satel) output_update_method({"outputs": {1: 1}}) assert hass.states.get("switch.switchable_output").state == STATE_ON + output_update_method({"outputs": {1: 0}}) + assert hass.states.get("switch.switchable_output").state == STATE_OFF + + # The client library should always report all entries, but test that we set the status correctly if it doesn't + output_update_method({"outputs": {2: 1}}) + assert hass.states.get("switch.switchable_output").state == STATE_UNKNOWN + async def test_switch_change_state( hass: HomeAssistant, From d12816d297a2cc3a1881e90657623f92c01aa115 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 17 Feb 2026 14:08:02 +0100 Subject: [PATCH 19/36] Removed more warnings from Fritz tests (#163262) --- homeassistant/components/fritz/__init__.py | 4 ++- tests/components/fritz/test_button.py | 33 +++++++++++++--------- tests/components/fritz/test_image.py | 13 +++++---- tests/components/fritz/test_init.py | 19 +++++++++++-- tests/components/fritz/test_sensor.py | 1 + tests/components/fritz/test_services.py | 8 ++++++ 6 files changed, 54 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 94f4f8ba0d894d..69be911f8f14c8 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -86,7 +86,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> bo avm_wrapper = entry.runtime_data fritz_data = hass.data[FRITZ_DATA_KEY] - fritz_data.tracked.pop(avm_wrapper.unique_id) + + if avm_wrapper.unique_id in fritz_data.tracked: + fritz_data.tracked.pop(avm_wrapper.unique_id) if not bool(fritz_data.tracked): hass.data.pop(FRITZ_DATA_KEY) diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py index 0bbd98b90bfa31..d2be55769e17b3 100644 --- a/tests/components/fritz/test_button.py +++ b/tests/components/fritz/test_button.py @@ -30,6 +30,7 @@ async def test_button_setup( entity_registry: er.EntityRegistry, fc_class_mock, fh_class_mock, + fs_class_mock, snapshot: SnapshotAssertion, ) -> None: """Test setup of Fritz!Tools buttons.""" @@ -59,6 +60,7 @@ async def test_buttons( wrapper_method: str, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test Fritz!Tools buttons.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) @@ -68,9 +70,9 @@ async def test_buttons( await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED - button = hass.states.get(entity_id) - assert button - assert button.state == STATE_UNKNOWN + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNKNOWN + with patch( f"homeassistant.components.fritz.coordinator.AvmWrapper.{wrapper_method}" ) as mock_press_action: @@ -82,8 +84,8 @@ async def test_buttons( ) mock_press_action.assert_called_once() - button = hass.states.get(entity_id) - assert button.state != STATE_UNKNOWN + assert (state := hass.states.get(entity_id)) + assert state.state != STATE_UNKNOWN @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -91,6 +93,7 @@ async def test_wol_button( hass: HomeAssistant, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test Fritz!Tools wake on LAN button.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) @@ -101,9 +104,9 @@ async def test_wol_button( assert entry.state is ConfigEntryState.LOADED - button = hass.states.get("button.printer_wake_on_lan") - assert button - assert button.state == STATE_UNKNOWN + assert (state := hass.states.get("button.printer_wake_on_lan")) + assert state.state == STATE_UNKNOWN + with patch( "homeassistant.components.fritz.coordinator.AvmWrapper.async_wake_on_lan" ) as mock_press_action: @@ -115,8 +118,8 @@ async def test_wol_button( ) mock_press_action.assert_called_once_with("AA:BB:CC:00:11:22") - button = hass.states.get("button.printer_wake_on_lan") - assert button.state != STATE_UNKNOWN + assert (state := hass.states.get("button.printer_wake_on_lan")) + assert state.state != STATE_UNKNOWN @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -125,6 +128,7 @@ async def test_wol_button_new_device( freezer: FrozenDateTimeFactory, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test WoL button is created for new device at runtime.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) @@ -154,6 +158,7 @@ async def test_wol_button_absent_for_mesh_slave( hass: HomeAssistant, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test WoL button not created if interviewed box is in slave mode.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) @@ -167,8 +172,7 @@ async def test_wol_button_absent_for_mesh_slave( await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED - button = hass.states.get("button.printer_wake_on_lan") - assert button is None + assert hass.states.get("button.printer_wake_on_lan") is None @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -176,6 +180,7 @@ async def test_wol_button_absent_for_non_lan_device( hass: HomeAssistant, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test WoL button not created if interviewed device is not connected via LAN.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) @@ -193,8 +198,7 @@ async def test_wol_button_absent_for_non_lan_device( await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED - button = hass.states.get("button.printer_wake_on_lan") - assert button is None + assert hass.states.get("button.printer_wake_on_lan") is None async def test_cleanup_button( @@ -203,6 +207,7 @@ async def test_cleanup_button( entity_registry: er.EntityRegistry, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test cleanup of orphan devices.""" diff --git a/tests/components/fritz/test_image.py b/tests/components/fritz/test_image.py index 837539e9fce279..0f42e2fd06fe51 100644 --- a/tests/components/fritz/test_image.py +++ b/tests/components/fritz/test_image.py @@ -125,8 +125,8 @@ async def test_image_entity( "friendly_name": "Mock Title GuestWifi", } - entity_entry = entity_registry.async_get("image.mock_title_guestwifi") - assert entity_entry.unique_id == "1c_ed_6f_12_34_11_guestwifi_qr_code" + assert (state := entity_registry.async_get("image.mock_title_guestwifi")) + assert state.unique_id == "1c_ed_6f_12_34_11_guestwifi_qr_code" # test image download client = await hass_client() @@ -187,6 +187,8 @@ async def test_image_update_unavailable( ) -> None: """Test image update when fritzbox is unavailable.""" + entity_id = "image.mock_title_guestwifi" + # setup component with image platform only with patch( "homeassistant.components.fritz.PLATFORMS", @@ -199,8 +201,7 @@ async def test_image_update_unavailable( await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED - state = hass.states.get("image.mock_title_guestwifi") - assert state + assert hass.states.get(entity_id) # fritzbox becomes unavailable fc_class_mock().call_action_side_effect(ReadTimeout) @@ -209,7 +210,7 @@ async def test_image_update_unavailable( async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get("image.mock_title_guestwifi") + assert (state := hass.states.get(entity_id)) assert state.state == STATE_UNKNOWN # fritzbox is available again @@ -219,5 +220,5 @@ async def test_image_update_unavailable( async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get("image.mock_title_guestwifi") + assert (state := hass.states.get(entity_id)) assert state.state != STATE_UNKNOWN diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py index 5b2dad5dfdc219..024160dad68c8d 100644 --- a/tests/components/fritz/test_init.py +++ b/tests/components/fritz/test_init.py @@ -25,7 +25,12 @@ from tests.common import MockConfigEntry, async_fire_time_changed -async def test_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) -> None: +async def test_setup( + hass: HomeAssistant, + fc_class_mock, + fh_class_mock, + fs_class_mock, +) -> None: """Test setup and unload of Fritz!Tools.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) @@ -40,7 +45,10 @@ async def test_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) -> None: async def test_options_reload( - hass: HomeAssistant, fc_class_mock, fh_class_mock + hass: HomeAssistant, + fc_class_mock, + fh_class_mock, + fs_class_mock, ) -> None: """Test reload of Fritz!Tools, when options changed.""" @@ -109,7 +117,11 @@ async def test_setup_fail(hass: HomeAssistant, error) -> None: async def test_upnp_missing( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, fc_class_mock, fh_class_mock + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + fc_class_mock, + fh_class_mock, + fs_class_mock, ) -> None: """Test UPNP configuration is missing.""" @@ -139,6 +151,7 @@ async def test_execute_action_while_shutdown( caplog: pytest.LogCaptureFixture, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test Fritz!Tools actions executed during shutdown of HomeAssistant.""" diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py index 4731b9845163f3..4b6af0d55d51c1 100644 --- a/tests/components/fritz/test_sensor.py +++ b/tests/components/fritz/test_sensor.py @@ -49,6 +49,7 @@ async def test_sensor_update_fail( freezer: FrozenDateTimeFactory, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test failed update of Fritz!Tools sensors.""" diff --git a/tests/components/fritz/test_services.py b/tests/components/fritz/test_services.py index 7407ddbe370903..2cb7b948514cea 100644 --- a/tests/components/fritz/test_services.py +++ b/tests/components/fritz/test_services.py @@ -68,6 +68,7 @@ async def test_service_set_guest_wifi_password_unknown_parameter( caplog: pytest.LogCaptureFixture, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test service set_guest_wifi_password with unknown parameter.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -98,6 +99,7 @@ async def test_service_set_guest_wifi_password_service_not_supported( caplog: pytest.LogCaptureFixture, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test service set_guest_wifi_password with connection error.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -149,6 +151,7 @@ async def test_service_dial( caplog: pytest.LogCaptureFixture, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test service dial.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -180,6 +183,7 @@ async def test_service_dial_unknown_parameter( caplog: pytest.LogCaptureFixture, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test service dial with unknown parameters.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -212,6 +216,7 @@ async def test_service_dial_wrong_parameter( caplog: pytest.LogCaptureFixture, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test service dial with unknown parameters.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -261,6 +266,7 @@ async def test_service_dial_service_not_supported( caplog: pytest.LogCaptureFixture, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test service dial with connection error.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -293,6 +299,7 @@ async def test_service_dial_failed( caplog: pytest.LogCaptureFixture, fc_class_mock, fh_class_mock, + fs_class_mock, ) -> None: """Test dial service when the dial help is disabled.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -325,6 +332,7 @@ async def test_service_dial_failed( async def test_service_dial_unloaded( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + fs_class_mock, ) -> None: """Test service dial.""" assert await async_setup_component(hass, DOMAIN, {}) From 98b8e152e3055296f3e89e2c2636b5282adbf7f9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:25:00 +0100 Subject: [PATCH 20/36] Use shorthand attributes in currencylayer (#163267) --- .../components/currencylayer/sensor.py | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index 7c985b12ba4733..832a856f51a973 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -65,33 +65,18 @@ class CurrencylayerSensor(SensorEntity): _attr_attribution = "Data provided by currencylayer.com" _attr_icon = "mdi:currency" - def __init__(self, rest, base, quote): + def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None: """Initialize the sensor.""" self.rest = rest - self._quote = quote - self._base = base - self._state = None - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._quote - - @property - def name(self): - """Return the name of the sensor.""" - return self._base - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state + self._attr_name = base + self._attr_native_unit_of_measurement = quote + self._key = f"{base}{quote}" def update(self) -> None: """Update current date.""" self.rest.update() if (value := self.rest.data) is not None: - self._state = round(value[f"{self._base}{self._quote}"], 4) + self._attr_native_value = round(value[self._key], 4) class CurrencylayerData: From 637accbfff4876936274934692dbfe51dcfb226c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:34:48 +0100 Subject: [PATCH 21/36] Rename DOMAIN_xxx aliases in template (#163259) --- homeassistant/components/template/config.py | 74 ++++++++++----------- tests/components/template/test_blueprint.py | 52 +++++++-------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index eecbd1a38f1c7a..cc261ce32888e4 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -9,27 +9,27 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - DOMAIN as DOMAIN_ALARM_CONTROL_PANEL, + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, ) -from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.blueprint import ( is_blueprint_instance_config, schemas as blueprint_schemas, ) -from homeassistant.components.button import DOMAIN as DOMAIN_BUTTON -from homeassistant.components.cover import DOMAIN as DOMAIN_COVER -from homeassistant.components.event import DOMAIN as DOMAIN_EVENT -from homeassistant.components.fan import DOMAIN as DOMAIN_FAN -from homeassistant.components.image import DOMAIN as DOMAIN_IMAGE -from homeassistant.components.light import DOMAIN as DOMAIN_LIGHT -from homeassistant.components.lock import DOMAIN as DOMAIN_LOCK -from homeassistant.components.number import DOMAIN as DOMAIN_NUMBER -from homeassistant.components.select import DOMAIN as DOMAIN_SELECT -from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR -from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH -from homeassistant.components.update import DOMAIN as DOMAIN_UPDATE -from homeassistant.components.vacuum import DOMAIN as DOMAIN_VACUUM -from homeassistant.components.weather import DOMAIN as DOMAIN_WEATHER +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.event import DOMAIN as EVENT_DOMAIN +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN +from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN +from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.config import async_log_schema_error, config_without_domain from homeassistant.const import ( CONF_ACTION, @@ -86,8 +86,8 @@ def validate_binary_sensor_auto_off_has_trigger(obj: dict) -> dict: """Validate that binary sensors with auto_off have triggers.""" - if CONF_TRIGGERS not in obj and DOMAIN_BINARY_SENSOR in obj: - binary_sensors: list[ConfigType] = obj[DOMAIN_BINARY_SENSOR] + if CONF_TRIGGERS not in obj and BINARY_SENSOR_DOMAIN in obj: + binary_sensors: list[ConfigType] = obj[BINARY_SENSOR_DOMAIN] for binary_sensor in binary_sensors: if binary_sensor_platform.CONF_AUTO_OFF not in binary_sensor: continue @@ -192,53 +192,53 @@ def _backward_compat_schema(value: Any | None) -> Any: vol.Optional(CONF_TRIGGERS): cv.TRIGGER_SCHEMA, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, - vol.Optional(DOMAIN_ALARM_CONTROL_PANEL): vol.All( + vol.Optional(ALARM_CONTROL_PANEL_DOMAIN): vol.All( cv.ensure_list, [alarm_control_panel_platform.ALARM_CONTROL_PANEL_YAML_SCHEMA], ), - vol.Optional(DOMAIN_BINARY_SENSOR): vol.All( + vol.Optional(BINARY_SENSOR_DOMAIN): vol.All( cv.ensure_list, [binary_sensor_platform.BINARY_SENSOR_YAML_SCHEMA] ), - vol.Optional(DOMAIN_BUTTON): vol.All( + vol.Optional(BUTTON_DOMAIN): vol.All( cv.ensure_list, [button_platform.BUTTON_YAML_SCHEMA] ), - vol.Optional(DOMAIN_COVER): vol.All( + vol.Optional(COVER_DOMAIN): vol.All( cv.ensure_list, [cover_platform.COVER_YAML_SCHEMA] ), - vol.Optional(DOMAIN_EVENT): vol.All( + vol.Optional(EVENT_DOMAIN): vol.All( cv.ensure_list, [event_platform.EVENT_YAML_SCHEMA] ), - vol.Optional(DOMAIN_FAN): vol.All( + vol.Optional(FAN_DOMAIN): vol.All( cv.ensure_list, [fan_platform.FAN_YAML_SCHEMA] ), - vol.Optional(DOMAIN_IMAGE): vol.All( + vol.Optional(IMAGE_DOMAIN): vol.All( cv.ensure_list, [image_platform.IMAGE_YAML_SCHEMA] ), - vol.Optional(DOMAIN_LIGHT): vol.All( + vol.Optional(LIGHT_DOMAIN): vol.All( cv.ensure_list, [light_platform.LIGHT_YAML_SCHEMA] ), - vol.Optional(DOMAIN_LOCK): vol.All( + vol.Optional(LOCK_DOMAIN): vol.All( cv.ensure_list, [lock_platform.LOCK_YAML_SCHEMA] ), - vol.Optional(DOMAIN_NUMBER): vol.All( + vol.Optional(NUMBER_DOMAIN): vol.All( cv.ensure_list, [number_platform.NUMBER_YAML_SCHEMA] ), - vol.Optional(DOMAIN_SELECT): vol.All( + vol.Optional(SELECT_DOMAIN): vol.All( cv.ensure_list, [select_platform.SELECT_YAML_SCHEMA] ), - vol.Optional(DOMAIN_SENSOR): vol.All( + vol.Optional(SENSOR_DOMAIN): vol.All( cv.ensure_list, [sensor_platform.SENSOR_YAML_SCHEMA] ), - vol.Optional(DOMAIN_SWITCH): vol.All( + vol.Optional(SWITCH_DOMAIN): vol.All( cv.ensure_list, [switch_platform.SWITCH_YAML_SCHEMA] ), - vol.Optional(DOMAIN_UPDATE): vol.All( + vol.Optional(UPDATE_DOMAIN): vol.All( cv.ensure_list, [update_platform.UPDATE_YAML_SCHEMA] ), - vol.Optional(DOMAIN_VACUUM): vol.All( + vol.Optional(VACUUM_DOMAIN): vol.All( cv.ensure_list, [vacuum_platform.VACUUM_YAML_SCHEMA] ), - vol.Optional(DOMAIN_WEATHER): vol.All( + vol.Optional(WEATHER_DOMAIN): vol.All( cv.ensure_list, [ vol.Any( @@ -250,7 +250,7 @@ def _backward_compat_schema(value: Any | None) -> Any: }, ), ensure_domains_do_not_have_trigger_or_action( - DOMAIN_BUTTON, + BUTTON_DOMAIN, ), validate_binary_sensor_auto_off_has_trigger, ) @@ -382,12 +382,12 @@ async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> Conf for old_key, new_key, legacy_fields in ( ( CONF_SENSORS, - DOMAIN_SENSOR, + SENSOR_DOMAIN, sensor_platform.LEGACY_FIELDS, ), ( CONF_BINARY_SENSORS, - DOMAIN_BINARY_SENSOR, + BINARY_SENSOR_DOMAIN, binary_sensor_platform.LEGACY_FIELDS, ), ): diff --git a/tests/components/template/test_blueprint.py b/tests/components/template/test_blueprint.py index 469e3df0ae06e0..79cd4f15c4f7e1 100644 --- a/tests/components/template/test_blueprint.py +++ b/tests/components/template/test_blueprint.py @@ -17,19 +17,19 @@ ) from homeassistant.components.template import DOMAIN, SERVICE_RELOAD from homeassistant.components.template.config import ( - DOMAIN_ALARM_CONTROL_PANEL, - DOMAIN_BINARY_SENSOR, - DOMAIN_COVER, - DOMAIN_FAN, - DOMAIN_IMAGE, - DOMAIN_LIGHT, - DOMAIN_LOCK, - DOMAIN_NUMBER, - DOMAIN_SELECT, - DOMAIN_SENSOR, - DOMAIN_SWITCH, - DOMAIN_VACUUM, - DOMAIN_WEATHER, + ALARM_CONTROL_PANEL_DOMAIN, + BINARY_SENSOR_DOMAIN, + COVER_DOMAIN, + FAN_DOMAIN, + IMAGE_DOMAIN, + LIGHT_DOMAIN, + LOCK_DOMAIN, + NUMBER_DOMAIN, + SELECT_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, + VACUUM_DOMAIN, + WEATHER_DOMAIN, ) from homeassistant.const import STATE_ON from homeassistant.core import Context, HomeAssistant, callback @@ -564,19 +564,19 @@ async def test_no_blueprint(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("domain", "set_state", "expected"), [ - (DOMAIN_ALARM_CONTROL_PANEL, STATE_ON, "armed_home"), - (DOMAIN_BINARY_SENSOR, STATE_ON, STATE_ON), - (DOMAIN_COVER, STATE_ON, "open"), - (DOMAIN_FAN, STATE_ON, STATE_ON), - (DOMAIN_IMAGE, "test.jpg", "2025-06-13T00:00:00+00:00"), - (DOMAIN_LIGHT, STATE_ON, STATE_ON), - (DOMAIN_LOCK, STATE_ON, "locked"), - (DOMAIN_NUMBER, "1", "1.0"), - (DOMAIN_SELECT, "option1", "option1"), - (DOMAIN_SENSOR, "foo", "foo"), - (DOMAIN_SWITCH, STATE_ON, STATE_ON), - (DOMAIN_VACUUM, "cleaning", "cleaning"), - (DOMAIN_WEATHER, "sunny", "sunny"), + (ALARM_CONTROL_PANEL_DOMAIN, STATE_ON, "armed_home"), + (BINARY_SENSOR_DOMAIN, STATE_ON, STATE_ON), + (COVER_DOMAIN, STATE_ON, "open"), + (FAN_DOMAIN, STATE_ON, STATE_ON), + (IMAGE_DOMAIN, "test.jpg", "2025-06-13T00:00:00+00:00"), + (LIGHT_DOMAIN, STATE_ON, STATE_ON), + (LOCK_DOMAIN, STATE_ON, "locked"), + (NUMBER_DOMAIN, "1", "1.0"), + (SELECT_DOMAIN, "option1", "option1"), + (SENSOR_DOMAIN, "foo", "foo"), + (SWITCH_DOMAIN, STATE_ON, STATE_ON), + (VACUUM_DOMAIN, "cleaning", "cleaning"), + (WEATHER_DOMAIN, "sunny", "sunny"), ], ) @pytest.mark.freeze_time("2025-06-13 00:00:00+00:00") From 163a6805ebe17cf19080b0965b610f7293d9f0e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:35:25 +0100 Subject: [PATCH 22/36] Rename DOMAIN_xxx aliases in components (#163260) --- homeassistant/components/alert/entity.py | 4 ++-- homeassistant/components/camera/__init__.py | 6 ++--- .../device_sun_light_trigger/__init__.py | 24 +++++++++---------- .../device_tracker/device_trigger.py | 8 +++---- .../google_assistant_sdk/helpers.py | 4 ++-- .../components/homekit/type_thermostats.py | 20 ++++++++-------- homeassistant/components/lcn/binary_sensor.py | 6 ++--- homeassistant/components/lcn/climate.py | 6 ++--- homeassistant/components/lcn/cover.py | 6 ++--- homeassistant/components/lcn/light.py | 6 ++--- homeassistant/components/lcn/scene.py | 6 ++--- homeassistant/components/lcn/sensor.py | 6 ++--- homeassistant/components/lcn/switch.py | 6 ++--- .../components/nasweb/alarm_control_panel.py | 4 ++-- homeassistant/components/nasweb/sensor.py | 4 ++-- homeassistant/components/nasweb/switch.py | 4 ++-- homeassistant/components/tts/entity.py | 4 ++-- homeassistant/components/tts/legacy.py | 4 ++-- homeassistant/components/vicare/__init__.py | 4 ++-- homeassistant/components/zeroconf/repairs.py | 4 ++-- 20 files changed, 68 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/alert/entity.py b/homeassistant/components/alert/entity.py index f4497e0f7ad725..a7f9f50f61e226 100644 --- a/homeassistant/components/alert/entity.py +++ b/homeassistant/components/alert/entity.py @@ -13,7 +13,7 @@ ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE, - DOMAIN as DOMAIN_NOTIFY, + DOMAIN as NOTIFY_DOMAIN, ) from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_ON from homeassistant.core import Event, EventStateChangedData, HassJob, HomeAssistant @@ -185,7 +185,7 @@ async def _send_notification_message(self, message: Any) -> None: for target in self._notifiers: try: await self.hass.services.async_call( - DOMAIN_NOTIFY, target, msg_payload, context=self._context + NOTIFY_DOMAIN, target, msg_payload, context=self._context ) except ServiceNotFound: LOGGER.error( diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 9362faa1093df4..16dd4432ecc32d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -27,7 +27,7 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, ) from homeassistant.components.stream import ( @@ -133,7 +133,7 @@ class CameraEntityFeature(IntFlag): CAMERA_SERVICE_SNAPSHOT: VolDictType = {vol.Required(ATTR_FILENAME): cv.template} CAMERA_SERVICE_PLAY_STREAM: VolDictType = { - vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP), + vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(MP_DOMAIN), vol.Optional(ATTR_FORMAT, default="hls"): vol.In(OUTPUT_FORMATS), } @@ -1044,7 +1044,7 @@ async def async_handle_play_stream_service( url = f"{get_url(hass)}{url}" await hass.services.async_call( - DOMAIN_MP, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: service_call.data[ATTR_MEDIA_PLAYER], diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index ee427eb1ba654d..b97f3cf32cf939 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -7,17 +7,17 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN as DOMAIN_DEVICE_TRACKER, + DOMAIN as DEVICE_TRACKER_DOMAIN, is_on as device_tracker_is_on, ) from homeassistant.components.group import get_entity_ids as group_get_entity_ids from homeassistant.components.light import ( ATTR_PROFILE, ATTR_TRANSITION, - DOMAIN as DOMAIN_LIGHT, + DOMAIN as LIGHT_DOMAIN, is_on as light_is_on, ) -from homeassistant.components.person import DOMAIN as DOMAIN_PERSON +from homeassistant.components.person import DOMAIN as PERSON_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, @@ -97,13 +97,13 @@ async def activate_automation( # noqa: C901 logger = logging.getLogger(__name__) if device_group is None: - device_entity_ids = hass.states.async_entity_ids(DOMAIN_DEVICE_TRACKER) + device_entity_ids = hass.states.async_entity_ids(DEVICE_TRACKER_DOMAIN) else: device_entity_ids = group_get_entity_ids( - hass, device_group, DOMAIN_DEVICE_TRACKER + hass, device_group, DEVICE_TRACKER_DOMAIN ) device_entity_ids.extend( - group_get_entity_ids(hass, device_group, DOMAIN_PERSON) + group_get_entity_ids(hass, device_group, PERSON_DOMAIN) ) if not device_entity_ids: @@ -112,9 +112,9 @@ async def activate_automation( # noqa: C901 # Get the light IDs from the specified group if light_group is None: - light_ids = hass.states.async_entity_ids(DOMAIN_LIGHT) + light_ids = hass.states.async_entity_ids(LIGHT_DOMAIN) else: - light_ids = group_get_entity_ids(hass, light_group, DOMAIN_LIGHT) + light_ids = group_get_entity_ids(hass, light_group, LIGHT_DOMAIN) if not light_ids: logger.error("No lights found to turn on") @@ -147,7 +147,7 @@ async def async_turn_on_before_sunset(light_id): if not anyone_home() or light_is_on(hass, light_id): return await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: light_id, @@ -222,7 +222,7 @@ def check_light_on_dev_state_change( logger.info("Home coming event for %s. Turning lights on", entity) hass.async_create_task( hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: light_ids, ATTR_PROFILE: light_profile}, ) @@ -241,7 +241,7 @@ def check_light_on_dev_state_change( if now > start_point + index * LIGHT_TRANSITION_TIME: hass.async_create_task( hass.services.async_call( - DOMAIN_LIGHT, SERVICE_TURN_ON, {ATTR_ENTITY_ID: light_id} + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: light_id} ) ) @@ -273,7 +273,7 @@ def turn_off_lights_when_all_leave(entity, old_state, new_state): logger.info("Everyone has left but there are lights on. Turning them off") hass.async_create_task( hass.services.async_call( - DOMAIN_LIGHT, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: light_ids} + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: light_ids} ) ) diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py index bcd2f0f23428aa..cb299236438aca 100644 --- a/homeassistant/components/device_tracker/device_trigger.py +++ b/homeassistant/components/device_tracker/device_trigger.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA -from homeassistant.components.zone import DOMAIN as DOMAIN_ZONE, trigger as zone +from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN, trigger as zone from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, @@ -31,7 +31,7 @@ { vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), - vol.Required(CONF_ZONE): cv.entity_domain(DOMAIN_ZONE), + vol.Required(CONF_ZONE): cv.entity_domain(ZONE_DOMAIN), } ) @@ -83,7 +83,7 @@ async def async_attach_trigger( event = zone.EVENT_LEAVE zone_config = { - CONF_PLATFORM: DOMAIN_ZONE, + CONF_PLATFORM: ZONE_DOMAIN, CONF_ENTITY_ID: config[CONF_ENTITY_ID], CONF_ZONE: config[CONF_ZONE], CONF_EVENT: event, @@ -100,7 +100,7 @@ async def async_get_trigger_capabilities( """List trigger capabilities.""" zones = { ent.entity_id: ent.name - for ent in sorted(hass.states.async_all(DOMAIN_ZONE), key=attrgetter("name")) + for ent in sorted(hass.states.async_all(ZONE_DOMAIN), key=attrgetter("name")) } return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index a3ced9fd68bccd..b8318436a3a50b 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -19,7 +19,7 @@ ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, MediaType, ) @@ -112,7 +112,7 @@ async def async_send_text_commands( ) ) await hass.services.async_call( - DOMAIN_MP, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_players, diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 9e7675b5774efc..ebf1bd97c5be60 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -26,7 +26,7 @@ DEFAULT_MAX_TEMP, DEFAULT_MIN_HUMIDITY, DEFAULT_MIN_TEMP, - DOMAIN as DOMAIN_CLIMATE, + DOMAIN as CLIMATE_DOMAIN, FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -49,7 +49,7 @@ HVACMode, ) from homeassistant.components.water_heater import ( - DOMAIN as DOMAIN_WATER_HEATER, + DOMAIN as WATER_HEATER_DOMAIN, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_WATER_HEATER, ) from homeassistant.const import ( @@ -388,13 +388,13 @@ def _set_fan_swing_mode(self, swing_on: int) -> None: _LOGGER.debug("%s: Set swing mode to %s", self.entity_id, swing_on) mode = self.swing_on_mode if swing_on else SWING_OFF params = {ATTR_ENTITY_ID: self.entity_id, ATTR_SWING_MODE: mode} - self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_SWING_MODE, params) + self.async_call_service(CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, params) def _set_fan_speed(self, speed: int) -> None: _LOGGER.debug("%s: Set fan speed to %s", self.entity_id, speed) mode = percentage_to_ordered_list_item(self.ordered_fan_speeds, speed - 1) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode} - self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE, params) + self.async_call_service(CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, params) def _get_on_mode(self) -> str: if self.ordered_fan_speeds: @@ -412,13 +412,13 @@ def _set_fan_active(self, active: int) -> None: return mode = self._get_on_mode() if active else self.fan_modes[FAN_OFF] params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode} - self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE, params) + self.async_call_service(CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, params) def _set_fan_auto(self, auto: int) -> None: _LOGGER.debug("%s: Set fan auto to %s", self.entity_id, auto) mode = self.fan_modes[FAN_AUTO] if auto else self._get_on_mode() params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode} - self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE, params) + self.async_call_service(CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, params) def _temperature_to_homekit(self, temp: float) -> float: return temperature_to_homekit(temp, self._unit) @@ -480,7 +480,7 @@ def _set_chars(self, char_values: dict[str, Any]) -> None: # `SERVICE_SET_HVAC_MODE_THERMOSTAT` before calling `SERVICE_SET_TEMPERATURE_THERMOSTAT` # to ensure the device is in the right mode before setting the temp. self.async_call_service( - DOMAIN_CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE_THERMOSTAT, params.copy(), ", ".join(events), @@ -557,7 +557,7 @@ def _set_chars(self, char_values: dict[str, Any]) -> None: if service: self.async_call_service( - DOMAIN_CLIMATE, + CLIMATE_DOMAIN, service, params, ", ".join(events), @@ -608,7 +608,7 @@ def set_target_humidity(self, value: float) -> None: _LOGGER.debug("%s: Set target humidity to %d", self.entity_id, value) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HUMIDITY: value} self.async_call_service( - DOMAIN_CLIMATE, SERVICE_SET_HUMIDITY, params, f"{value}{PERCENTAGE}" + CLIMATE_DOMAIN, SERVICE_SET_HUMIDITY, params, f"{value}{PERCENTAGE}" ) @callback @@ -804,7 +804,7 @@ def set_target_temperature(self, value: float) -> None: temperature = temperature_to_states(value, self._unit) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TEMPERATURE: temperature} self.async_call_service( - DOMAIN_WATER_HEATER, + WATER_HEATER_DOMAIN, SERVICE_SET_TEMPERATURE_WATER_HEATER, params, f"{temperature}{self._unit}", diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 4f813ca4c00206..889bbaff5421b7 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -7,7 +7,7 @@ import pypck from homeassistant.components.binary_sensor import ( - DOMAIN as DOMAIN_BINARY_SENSOR, + DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorEntity, ) from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES, CONF_SOURCE @@ -48,14 +48,14 @@ async def async_setup_entry( ) config_entry.runtime_data.add_entities_callbacks.update( - {DOMAIN_BINARY_SENSOR: add_entities} + {BINARY_SENSOR_DOMAIN: add_entities} ) add_entities( ( entity_config for entity_config in config_entry.data[CONF_ENTITIES] - if entity_config[CONF_DOMAIN] == DOMAIN_BINARY_SENSOR + if entity_config[CONF_DOMAIN] == BINARY_SENSOR_DOMAIN ), ) diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 260c9bd3bf0292..aa633adf100173 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -8,7 +8,7 @@ import pypck from homeassistant.components.climate import ( - DOMAIN as DOMAIN_CLIMATE, + DOMAIN as CLIMATE_DOMAIN, ClimateEntity, ClimateEntityFeature, HVACMode, @@ -66,14 +66,14 @@ async def async_setup_entry( ) config_entry.runtime_data.add_entities_callbacks.update( - {DOMAIN_CLIMATE: add_entities} + {CLIMATE_DOMAIN: add_entities} ) add_entities( ( entity_config for entity_config in config_entry.data[CONF_ENTITIES] - if entity_config[CONF_DOMAIN] == DOMAIN_CLIMATE + if entity_config[CONF_DOMAIN] == CLIMATE_DOMAIN ), ) diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index 4066cef747fd55..ea2c1e6d82bc5b 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -9,7 +9,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, - DOMAIN as DOMAIN_COVER, + DOMAIN as COVER_DOMAIN, CoverEntity, CoverEntityFeature, ) @@ -60,14 +60,14 @@ async def async_setup_entry( ) config_entry.runtime_data.add_entities_callbacks.update( - {DOMAIN_COVER: add_entities} + {COVER_DOMAIN: add_entities} ) add_entities( ( entity_config for entity_config in config_entry.data[CONF_ENTITIES] - if entity_config[CONF_DOMAIN] == DOMAIN_COVER + if entity_config[CONF_DOMAIN] == COVER_DOMAIN ), ) diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index be6ac6935cdad8..b29f7fd2a00b5c 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, - DOMAIN as DOMAIN_LIGHT, + DOMAIN as LIGHT_DOMAIN, ColorMode, LightEntity, LightEntityFeature, @@ -66,14 +66,14 @@ async def async_setup_entry( ) config_entry.runtime_data.add_entities_callbacks.update( - {DOMAIN_LIGHT: add_entities} + {LIGHT_DOMAIN: add_entities} ) add_entities( ( entity_config for entity_config in config_entry.data[CONF_ENTITIES] - if entity_config[CONF_DOMAIN] == DOMAIN_LIGHT + if entity_config[CONF_DOMAIN] == LIGHT_DOMAIN ), ) diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py index e2089cda950c51..e8c09ec10815fe 100644 --- a/homeassistant/components/lcn/scene.py +++ b/homeassistant/components/lcn/scene.py @@ -6,7 +6,7 @@ import pypck -from homeassistant.components.scene import DOMAIN as DOMAIN_SCENE, Scene +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, Scene from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES, CONF_SCENE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -51,14 +51,14 @@ async def async_setup_entry( ) config_entry.runtime_data.add_entities_callbacks.update( - {DOMAIN_SCENE: add_entities} + {SCENE_DOMAIN: add_entities} ) add_entities( ( entity_config for entity_config in config_entry.data[CONF_ENTITIES] - if entity_config[CONF_DOMAIN] == DOMAIN_SCENE + if entity_config[CONF_DOMAIN] == SCENE_DOMAIN ), ) diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 3515d6ab5f574c..6b5c8bbfead5a3 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -8,7 +8,7 @@ import pypck from homeassistant.components.sensor import ( - DOMAIN as DOMAIN_SENSOR, + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, ) @@ -102,14 +102,14 @@ async def async_setup_entry( ) config_entry.runtime_data.add_entities_callbacks.update( - {DOMAIN_SENSOR: add_entities} + {SENSOR_DOMAIN: add_entities} ) add_entities( ( entity_config for entity_config in config_entry.data[CONF_ENTITIES] - if entity_config[CONF_DOMAIN] == DOMAIN_SENSOR + if entity_config[CONF_DOMAIN] == SENSOR_DOMAIN ), ) diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index c18c92215a95c3..2a71080c643b2d 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -7,7 +7,7 @@ import pypck -from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH, SwitchEntity +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -56,14 +56,14 @@ async def async_setup_entry( ) config_entry.runtime_data.add_entities_callbacks.update( - {DOMAIN_SWITCH: add_entities} + {SWITCH_DOMAIN: add_entities} ) add_entities( ( entity_config for entity_config in config_entry.data[CONF_ENTITIES] - if entity_config[CONF_DOMAIN] == DOMAIN_SWITCH + if entity_config[CONF_DOMAIN] == SWITCH_DOMAIN ), ) diff --git a/homeassistant/components/nasweb/alarm_control_panel.py b/homeassistant/components/nasweb/alarm_control_panel.py index 1c64eab0f07e2c..695c0168886dc6 100644 --- a/homeassistant/components/nasweb/alarm_control_panel.py +++ b/homeassistant/components/nasweb/alarm_control_panel.py @@ -9,7 +9,7 @@ from webio_api.const import STATE_ZONE_ALARM, STATE_ZONE_ARMED, STATE_ZONE_DISARMED from homeassistant.components.alarm_control_panel import ( - DOMAIN as DOMAIN_ALARM_CONTROL_PANEL, + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, AlarmControlPanelEntity, AlarmControlPanelEntityFeature, AlarmControlPanelState, @@ -69,7 +69,7 @@ def _check_entities() -> None: for index in removed: unique_id = f"{DOMAIN}.{config.unique_id}.zone.{index}" if entity_id := entity_registry.async_get_entity_id( - DOMAIN_ALARM_CONTROL_PANEL, DOMAIN, unique_id + ALARM_CONTROL_PANEL_DOMAIN, DOMAIN, unique_id ): entity_registry.async_remove(entity_id) current_zones.remove(index) diff --git a/homeassistant/components/nasweb/sensor.py b/homeassistant/components/nasweb/sensor.py index e01e401b2ba9d4..82a69b74aa6c89 100644 --- a/homeassistant/components/nasweb/sensor.py +++ b/homeassistant/components/nasweb/sensor.py @@ -15,7 +15,7 @@ ) from homeassistant.components.sensor import ( - DOMAIN as DOMAIN_SENSOR, + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -70,7 +70,7 @@ def _check_entities() -> None: for index in removed: unique_id = f"{DOMAIN}.{config.unique_id}.input.{index}" if entity_id := entity_registry.async_get_entity_id( - DOMAIN_SENSOR, DOMAIN, unique_id + SENSOR_DOMAIN, DOMAIN, unique_id ): entity_registry.async_remove(entity_id) current_inputs.remove(index) diff --git a/homeassistant/components/nasweb/switch.py b/homeassistant/components/nasweb/switch.py index 06d3f57121e664..a36f3062932c2e 100644 --- a/homeassistant/components/nasweb/switch.py +++ b/homeassistant/components/nasweb/switch.py @@ -9,7 +9,7 @@ from webio_api import Output as NASwebOutput from webio_api.const import STATE_ENTITY_UNAVAILABLE, STATE_OUTPUT_OFF, STATE_OUTPUT_ON -from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH, SwitchEntity +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo @@ -71,7 +71,7 @@ def _check_entities() -> None: for index in removed: unique_id = f"{DOMAIN}.{config.unique_id}.relay_switch.{index}" if entity_id := entity_registry.async_get_entity_id( - DOMAIN_SWITCH, DOMAIN, unique_id + SWITCH_DOMAIN, DOMAIN, unique_id ): entity_registry.async_remove(entity_id) current_outputs.remove(index) diff --git a/homeassistant/components/tts/entity.py b/homeassistant/components/tts/entity.py index 77abaa26baba5c..3b5f29bba4449f 100644 --- a/homeassistant/components/tts/entity.py +++ b/homeassistant/components/tts/entity.py @@ -11,7 +11,7 @@ ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, MediaType, ) @@ -134,7 +134,7 @@ async def async_speak( ) -> None: """Speak via a Media Player.""" await self.hass.services.async_call( - DOMAIN_MP, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_entity_id, diff --git a/homeassistant/components/tts/legacy.py b/homeassistant/components/tts/legacy.py index c3d7eb6fdd65b5..edae942a1d472a 100644 --- a/homeassistant/components/tts/legacy.py +++ b/homeassistant/components/tts/legacy.py @@ -15,7 +15,7 @@ ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, MediaType, ) @@ -153,7 +153,7 @@ async def async_say_handle(service: ServiceCall) -> None: entity_ids = service.data[ATTR_ENTITY_ID] await hass.services.async_call( - DOMAIN_MP, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: entity_ids, diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 2b96a7ad8e863a..e29b02071b86b5 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -13,7 +13,7 @@ PyViCareInvalidCredentialsError, ) -from homeassistant.components.climate import DOMAIN as DOMAIN_CLIMATE +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -145,7 +145,7 @@ async def async_migrate_devices_and_entities( # convert climate entity unique id # from `-` # to `-heating-` - if entity_entry.domain == DOMAIN_CLIMATE: + if entity_entry.domain == CLIMATE_DOMAIN: unique_id_parts[len(unique_id_parts) - 1] = ( f"{entity_entry.translation_key}-" f"{unique_id_parts[len(unique_id_parts) - 1]}" diff --git a/homeassistant/components/zeroconf/repairs.py b/homeassistant/components/zeroconf/repairs.py index 3afde331a42fb3..2af53ff46257bb 100644 --- a/homeassistant/components/zeroconf/repairs.py +++ b/homeassistant/components/zeroconf/repairs.py @@ -4,7 +4,7 @@ from homeassistant import data_entry_flow from homeassistant.components.homeassistant import ( - DOMAIN as DOMAIN_HOMEASSISTANT, + DOMAIN as HOMEASSISTANT_DOMAIN, SERVICE_HOMEASSISTANT_RESTART, ) from homeassistant.components.repairs import RepairsFlow @@ -35,7 +35,7 @@ async def async_step_confirm_recreate( if user_input is not None: await instance_id.async_recreate(self.hass) await self.hass.services.async_call( - DOMAIN_HOMEASSISTANT, SERVICE_HOMEASSISTANT_RESTART + HOMEASSISTANT_DOMAIN, SERVICE_HOMEASSISTANT_RESTART ) return self.async_create_entry(title="", data={}) From b6e7a55cd15cd8f90cb287e915a852c1240ffb04 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:37:17 +0100 Subject: [PATCH 23/36] Rename DOMAIN_xxx aliases in tests (#163261) --- tests/components/api/test_init.py | 14 +- tests/components/cloud/test_tts.py | 4 +- tests/components/elevenlabs/test_tts.py | 4 +- tests/components/fish_audio/test_tts.py | 4 +- .../test_tts.py | 4 +- .../homekit/test_type_thermostats.py | 50 +++--- tests/components/lcn/test_climate.py | 18 +-- tests/components/lcn/test_cover.py | 38 ++--- tests/components/lcn/test_light.py | 24 +-- tests/components/lcn/test_scene.py | 4 +- tests/components/lcn/test_switch.py | 42 ++--- tests/components/marytts/test_tts.py | 8 +- .../openai_conversation/test_tts.py | 4 +- tests/components/tts/test_init.py | 36 ++--- tests/components/tts/test_legacy.py | 4 +- tests/components/tts/test_notify.py | 6 +- tests/components/voicerss/test_tts.py | 14 +- .../components/websocket_api/test_commands.py | 20 +-- tests/components/yandextts/test_tts.py | 22 +-- tests/helpers/test_condition.py | 24 +-- tests/helpers/test_service.py | 148 +++++++++--------- tests/helpers/test_trigger.py | 26 +-- 22 files changed, 259 insertions(+), 259 deletions(-) diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index 691d270f524bbd..93965c6d3dac45 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -15,9 +15,9 @@ from homeassistant import const, core as ha from homeassistant.auth.models import Credentials from homeassistant.bootstrap import DATA_LOGGING -from homeassistant.components.group import DOMAIN as DOMAIN_GROUP -from homeassistant.components.logger import DOMAIN as DOMAIN_LOGGER -from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH +from homeassistant.components.group import DOMAIN as GROUP_DOMAIN +from homeassistant.components.logger import DOMAIN as LOGGER_DOMAIN +from homeassistant.components.system_health import DOMAIN as SYSTEM_HEALTH_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.loader import Integration from homeassistant.setup import async_setup_component @@ -327,10 +327,10 @@ async def test_api_get_services( ) -> None: """Test if we can get a dict describing current services.""" # Set up an integration that has services - assert await async_setup_component(hass, DOMAIN_GROUP, {DOMAIN_GROUP: {}}) + assert await async_setup_component(hass, GROUP_DOMAIN, {GROUP_DOMAIN: {}}) # Set up an integration that has no services - assert await async_setup_component(hass, DOMAIN_SYSTEM_HEALTH, {}) + assert await async_setup_component(hass, SYSTEM_HEALTH_DOMAIN, {}) resp = await mock_api_client.get(const.URL_API_SERVICES) data = await resp.json() @@ -367,7 +367,7 @@ def _load_services_file(integration: Integration) -> JSON_TYPE: "set_level": None, } - await async_setup_component(hass, DOMAIN_LOGGER, {DOMAIN_LOGGER: {}}) + await async_setup_component(hass, LOGGER_DOMAIN, {LOGGER_DOMAIN: {}}) await hass.async_block_till_done() with ( @@ -380,7 +380,7 @@ def _load_services_file(integration: Integration) -> JSON_TYPE: data2 = await resp.json() - assert data2 == [*data, {"domain": DOMAIN_LOGGER, "services": ANY}] + assert data2 == [*data, {"domain": LOGGER_DOMAIN, "services": ANY}] assert data2[-1] == snapshot diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index ff7915ceee5437..ccc67570212ccd 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -25,7 +25,7 @@ ) from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, ) from homeassistant.components.tts import ( @@ -857,7 +857,7 @@ async def test_tts_services( service_data: dict[str, Any], ) -> None: """Test tts services.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) mock_process_tts = AsyncMock(return_value=b"") cloud.voice.process_tts = mock_process_tts mock_process_tts_stream = _make_stream_mock("There is someone at the door.") diff --git a/tests/components/elevenlabs/test_tts.py b/tests/components/elevenlabs/test_tts.py index 08ab1b6fab0ed7..7b9140f7653178 100644 --- a/tests/components/elevenlabs/test_tts.py +++ b/tests/components/elevenlabs/test_tts.py @@ -28,7 +28,7 @@ ) from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, ) from homeassistant.components.tts import TTSAudioRequest @@ -149,7 +149,7 @@ def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: @pytest.fixture async def calls(hass: HomeAssistant) -> list[ServiceCall]: """Mock media player calls.""" - return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + return async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) @pytest.fixture(autouse=True) diff --git a/tests/components/fish_audio/test_tts.py b/tests/components/fish_audio/test_tts.py index 73f983854cb2e2..800980b8a2d4c4 100644 --- a/tests/components/fish_audio/test_tts.py +++ b/tests/components/fish_audio/test_tts.py @@ -14,7 +14,7 @@ from homeassistant.components.fish_audio.const import CONF_BACKEND from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, ) from homeassistant.config_entries import ConfigSubentryData @@ -49,7 +49,7 @@ async def setup_internal_url(hass: HomeAssistant) -> None: @pytest.fixture async def calls(hass: HomeAssistant) -> list[ServiceCall]: """Mock media player calls.""" - return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + return async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) async def test_tts_service_success( diff --git a/tests/components/google_generative_ai_conversation/test_tts.py b/tests/components/google_generative_ai_conversation/test_tts.py index c55a2a2795d369..ef133886089bb0 100644 --- a/tests/components/google_generative_ai_conversation/test_tts.py +++ b/tests/components/google_generative_ai_conversation/test_tts.py @@ -24,7 +24,7 @@ ) from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, ) from homeassistant.config_entries import ConfigSubentry @@ -54,7 +54,7 @@ def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: @pytest.fixture async def calls(hass: HomeAssistant) -> list[ServiceCall]: """Mock media player calls.""" - return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + return async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) @pytest.fixture(autouse=True) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 4d07757baf3613..d8a24bcbb3be2f 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -27,7 +27,7 @@ DEFAULT_MAX_TEMP, DEFAULT_MIN_HUMIDITY, DEFAULT_MIN_TEMP, - DOMAIN as DOMAIN_CLIMATE, + DOMAIN as CLIMATE_DOMAIN, FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -66,7 +66,7 @@ Thermostat, WaterHeater, ) -from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER +from homeassistant.components.water_heater import DOMAIN as WATER_HEATER_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -331,8 +331,8 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events: list[Event]) - assert acc.char_display_units.value == 0 # Set from HomeKit - call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature") - call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + call_set_temperature = async_mock_service(hass, CLIMATE_DOMAIN, "set_temperature") + call_set_hvac_mode = async_mock_service(hass, CLIMATE_DOMAIN, "set_hvac_mode") char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID] char_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] @@ -511,7 +511,7 @@ async def test_thermostat_auto( assert acc.char_display_units.value == 0 # Set from HomeKit - call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature") + call_set_temperature = async_mock_service(hass, CLIMATE_DOMAIN, "set_temperature") char_heating_thresh_temp_iid = acc.char_heating_thresh_temp.to_HAP()[HAP_REPR_IID] char_cooling_thresh_temp_iid = acc.char_cooling_thresh_temp.to_HAP()[HAP_REPR_IID] @@ -608,8 +608,8 @@ async def test_thermostat_mode_and_temp_change( assert acc.char_display_units.value == 0 # Set from HomeKit - call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature") - call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + call_set_temperature = async_mock_service(hass, CLIMATE_DOMAIN, "set_temperature") + call_set_hvac_mode = async_mock_service(hass, CLIMATE_DOMAIN, "set_hvac_mode") char_heating_thresh_temp_iid = acc.char_heating_thresh_temp.to_HAP()[HAP_REPR_IID] char_cooling_thresh_temp_iid = acc.char_cooling_thresh_temp.to_HAP()[HAP_REPR_IID] @@ -695,7 +695,7 @@ async def test_thermostat_humidity( assert acc.char_target_humidity.value == 35 # Set from HomeKit - call_set_humidity = async_mock_service(hass, DOMAIN_CLIMATE, "set_humidity") + call_set_humidity = async_mock_service(hass, CLIMATE_DOMAIN, "set_humidity") char_target_humidity_iid = acc.char_target_humidity.to_HAP()[HAP_REPR_IID] @@ -809,7 +809,7 @@ async def test_thermostat_power_state( assert acc.char_target_heat_cool.value == 0 # Set from HomeKit - call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + call_set_hvac_mode = async_mock_service(hass, CLIMATE_DOMAIN, "set_hvac_mode") char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] @@ -901,7 +901,7 @@ async def test_thermostat_fahrenheit( assert acc.char_display_units.value == 1 # Set from HomeKit - call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature") + call_set_temperature = async_mock_service(hass, CLIMATE_DOMAIN, "set_temperature") char_cooling_thresh_temp_iid = acc.char_cooling_thresh_temp.to_HAP()[HAP_REPR_IID] char_heating_thresh_temp_iid = acc.char_heating_thresh_temp.to_HAP()[HAP_REPR_IID] @@ -1117,7 +1117,7 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool( ] }, ) - call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + call_set_hvac_mode = async_mock_service(hass, CLIMATE_DOMAIN, "set_hvac_mode") await hass.async_block_till_done() acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) @@ -1175,7 +1175,7 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool( HVACMode.HEAT, {ATTR_HVAC_MODES: [HVACMode.AUTO, HVACMode.HEAT, HVACMode.OFF]}, ) - call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + call_set_hvac_mode = async_mock_service(hass, CLIMATE_DOMAIN, "set_hvac_mode") await hass.async_block_till_done() acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) @@ -1201,7 +1201,7 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool( assert acc.char_target_heat_cool.value == 1 char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] - call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + call_set_hvac_mode = async_mock_service(hass, CLIMATE_DOMAIN, "set_hvac_mode") await hass.async_block_till_done() hk_driver.set_characteristics( { @@ -1258,7 +1258,7 @@ async def test_thermostat_hvac_modes_with_auto_only( assert acc.char_target_heat_cool.value == 3 char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] - call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + call_set_hvac_mode = async_mock_service(hass, CLIMATE_DOMAIN, "set_hvac_mode") await hass.async_block_till_done() hk_driver.set_characteristics( { @@ -1315,7 +1315,7 @@ async def test_thermostat_hvac_modes_with_heat_only( assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] - call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + call_set_hvac_mode = async_mock_service(hass, CLIMATE_DOMAIN, "set_hvac_mode") await hass.async_block_till_done() hk_driver.set_characteristics( { @@ -1394,7 +1394,7 @@ async def test_thermostat_hvac_modes_with_cool_only( assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] - call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + call_set_hvac_mode = async_mock_service(hass, CLIMATE_DOMAIN, "set_hvac_mode") hk_driver.set_characteristics( { HAP_REPR_CHARS: [ @@ -1457,7 +1457,7 @@ async def test_thermostat_hvac_modes_with_heat_cool_only( assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID] char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] - call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + call_set_hvac_mode = async_mock_service(hass, CLIMATE_DOMAIN, "set_hvac_mode") hk_driver.set_characteristics( { HAP_REPR_CHARS: [ @@ -1633,7 +1633,7 @@ async def test_thermostat_without_target_temp_only_range( assert acc.char_display_units.value == 0 # Set from HomeKit - call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature") + call_set_temperature = async_mock_service(hass, CLIMATE_DOMAIN, "set_temperature") char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID] @@ -1679,7 +1679,7 @@ async def test_thermostat_without_target_temp_only_range( assert acc.char_display_units.value == 0 # Set from HomeKit - call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature") + call_set_temperature = async_mock_service(hass, CLIMATE_DOMAIN, "set_temperature") char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID] @@ -1760,7 +1760,7 @@ async def test_water_heater( # Set from HomeKit call_set_temperature = async_mock_service( - hass, DOMAIN_WATER_HEATER, "set_temperature" + hass, WATER_HEATER_DOMAIN, "set_temperature" ) acc.char_target_temp.client_update_value(52.0) @@ -1803,7 +1803,7 @@ async def test_water_heater_fahrenheit( # Set from HomeKit call_set_temperature = async_mock_service( - hass, DOMAIN_WATER_HEATER, "set_temperature" + hass, WATER_HEATER_DOMAIN, "set_temperature" ) acc.char_target_temp.client_update_value(60) @@ -2127,7 +2127,7 @@ async def test_thermostat_with_fan_modes_with_auto( assert acc.char_speed.value == pytest.approx(100 / 3) call_set_swing_mode = async_mock_service( - hass, DOMAIN_CLIMATE, SERVICE_SET_SWING_MODE + hass, CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE ) char_swing_iid = acc.char_swing.to_HAP()[HAP_REPR_IID] @@ -2167,7 +2167,7 @@ async def test_thermostat_with_fan_modes_with_auto( assert call_set_swing_mode[-1].data[ATTR_ENTITY_ID] == entity_id assert call_set_swing_mode[-1].data[ATTR_SWING_MODE] == SWING_BOTH - call_set_fan_mode = async_mock_service(hass, DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE) + call_set_fan_mode = async_mock_service(hass, CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE) char_rotation_speed_iid = acc.char_speed.to_HAP()[HAP_REPR_IID] hk_driver.set_characteristics( @@ -2332,7 +2332,7 @@ async def test_thermostat_with_fan_modes_with_off( await hass.async_block_till_done() assert acc.char_active.value == 0 - call_set_fan_mode = async_mock_service(hass, DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE) + call_set_fan_mode = async_mock_service(hass, CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE) char_active_iid = acc.char_active.to_HAP()[HAP_REPR_IID] hk_driver.set_characteristics( { @@ -2610,7 +2610,7 @@ async def test_thermostat_handles_unknown_state(hass: HomeAssistant, hk_driver) ], } - call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + call_set_hvac_mode = async_mock_service(hass, CLIMATE_DOMAIN, "set_hvac_mode") hass.states.async_set( entity_id, HVACMode.OFF, diff --git a/tests/components/lcn/test_climate.py b/tests/components/lcn/test_climate.py index c4dc21d787a7a6..a99137fd15bd3a 100644 --- a/tests/components/lcn/test_climate.py +++ b/tests/components/lcn/test_climate.py @@ -14,7 +14,7 @@ ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - DOMAIN as DOMAIN_CLIMATE, + DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, SERVICE_SET_TEMPERATURE, HVACMode, @@ -57,7 +57,7 @@ async def test_set_hvac_mode_heat(hass: HomeAssistant, entry: MockConfigEntry) - with patch.object(MockDeviceConnection, "lock_regulator") as lock_regulator: await hass.services.async_call( - DOMAIN_CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: CLIMATE_CLIMATE1, @@ -70,7 +70,7 @@ async def test_set_hvac_mode_heat(hass: HomeAssistant, entry: MockConfigEntry) - lock_regulator.return_value = False await hass.services.async_call( - DOMAIN_CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: CLIMATE_CLIMATE1, @@ -90,7 +90,7 @@ async def test_set_hvac_mode_heat(hass: HomeAssistant, entry: MockConfigEntry) - lock_regulator.return_value = True await hass.services.async_call( - DOMAIN_CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: CLIMATE_CLIMATE1, @@ -118,7 +118,7 @@ async def test_set_hvac_mode_off(hass: HomeAssistant, entry: MockConfigEntry) -> lock_regulator.return_value = False await hass.services.async_call( - DOMAIN_CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: CLIMATE_CLIMATE1, @@ -138,7 +138,7 @@ async def test_set_hvac_mode_off(hass: HomeAssistant, entry: MockConfigEntry) -> lock_regulator.return_value = True await hass.services.async_call( - DOMAIN_CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: CLIMATE_CLIMATE1, @@ -167,7 +167,7 @@ async def test_set_temperature(hass: HomeAssistant, entry: MockConfigEntry) -> N with pytest.raises(ServiceValidationError): await hass.services.async_call( - DOMAIN_CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: CLIMATE_CLIMATE1, @@ -184,7 +184,7 @@ async def test_set_temperature(hass: HomeAssistant, entry: MockConfigEntry) -> N var_abs.return_value = False await hass.services.async_call( - DOMAIN_CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: CLIMATE_CLIMATE1, ATTR_TEMPERATURE: 25.5}, blocking=True, @@ -201,7 +201,7 @@ async def test_set_temperature(hass: HomeAssistant, entry: MockConfigEntry) -> N var_abs.return_value = True await hass.services.async_call( - DOMAIN_CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: CLIMATE_CLIMATE1, ATTR_TEMPERATURE: 25.5}, blocking=True, diff --git a/tests/components/lcn/test_cover.py b/tests/components/lcn/test_cover.py index b7f82c8cdda6c0..5b60096759dd3e 100644 --- a/tests/components/lcn/test_cover.py +++ b/tests/components/lcn/test_cover.py @@ -17,7 +17,7 @@ from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, - DOMAIN as DOMAIN_COVER, + DOMAIN as COVER_DOMAIN, CoverState, ) from homeassistant.components.lcn.cover import SCAN_INTERVAL @@ -72,7 +72,7 @@ async def test_outputs_open(hass: HomeAssistant, entry: MockConfigEntry) -> None control_motor_outputs.return_value = False await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, @@ -91,7 +91,7 @@ async def test_outputs_open(hass: HomeAssistant, entry: MockConfigEntry) -> None control_motor_outputs.return_value = True await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, @@ -114,7 +114,7 @@ async def test_outputs_close(hass: HomeAssistant, entry: MockConfigEntry) -> Non MockDeviceConnection, "control_motor_outputs" ) as control_motor_outputs: await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, @@ -124,7 +124,7 @@ async def test_outputs_close(hass: HomeAssistant, entry: MockConfigEntry) -> Non control_motor_outputs.return_value = False await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, @@ -143,7 +143,7 @@ async def test_outputs_close(hass: HomeAssistant, entry: MockConfigEntry) -> Non control_motor_outputs.return_value = True await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, @@ -166,7 +166,7 @@ async def test_outputs_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None MockDeviceConnection, "control_motor_outputs" ) as control_motor_outputs: await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, @@ -176,7 +176,7 @@ async def test_outputs_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None control_motor_outputs.return_value = False await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, @@ -193,7 +193,7 @@ async def test_outputs_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None control_motor_outputs.return_value = True await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, @@ -221,7 +221,7 @@ async def test_relays_open(hass: HomeAssistant, entry: MockConfigEntry) -> None: control_motor_relays.return_value = False await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, @@ -240,7 +240,7 @@ async def test_relays_open(hass: HomeAssistant, entry: MockConfigEntry) -> None: control_motor_relays.return_value = True await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, @@ -263,7 +263,7 @@ async def test_relays_close(hass: HomeAssistant, entry: MockConfigEntry) -> None MockDeviceConnection, "control_motor_relays" ) as control_motor_relays: await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, @@ -273,7 +273,7 @@ async def test_relays_close(hass: HomeAssistant, entry: MockConfigEntry) -> None control_motor_relays.return_value = False await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, @@ -292,7 +292,7 @@ async def test_relays_close(hass: HomeAssistant, entry: MockConfigEntry) -> None control_motor_relays.return_value = True await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, @@ -315,7 +315,7 @@ async def test_relays_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None: MockDeviceConnection, "control_motor_relays" ) as control_motor_relays: await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, @@ -325,7 +325,7 @@ async def test_relays_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None: control_motor_relays.return_value = False await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, @@ -344,7 +344,7 @@ async def test_relays_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None: control_motor_relays.return_value = True await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, @@ -387,7 +387,7 @@ async def test_relays_set_position( control_motor_relays_position.return_value = False await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {ATTR_ENTITY_ID: entity_id, ATTR_POSITION: 50}, blocking=True, @@ -405,7 +405,7 @@ async def test_relays_set_position( control_motor_relays_position.return_value = True await hass.services.async_call( - DOMAIN_COVER, + COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {ATTR_ENTITY_ID: entity_id, ATTR_POSITION: 50}, blocking=True, diff --git a/tests/components/lcn/test_light.py b/tests/components/lcn/test_light.py index 55f8a7ce7b7735..e10fae7e9757b0 100644 --- a/tests/components/lcn/test_light.py +++ b/tests/components/lcn/test_light.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, - DOMAIN as DOMAIN_LIGHT, + DOMAIN as LIGHT_DOMAIN, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -59,7 +59,7 @@ async def test_output_turn_on(hass: HomeAssistant, entry: MockConfigEntry) -> No toggle_output.return_value = False await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: LIGHT_OUTPUT1}, blocking=True, @@ -76,7 +76,7 @@ async def test_output_turn_on(hass: HomeAssistant, entry: MockConfigEntry) -> No toggle_output.return_value = True await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: LIGHT_OUTPUT1}, blocking=True, @@ -99,7 +99,7 @@ async def test_output_turn_on_with_attributes( dim_output.return_value = True await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: LIGHT_OUTPUT1, @@ -122,7 +122,7 @@ async def test_output_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> N with patch.object(MockDeviceConnection, "toggle_output") as toggle_output: await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: LIGHT_OUTPUT1}, blocking=True, @@ -132,7 +132,7 @@ async def test_output_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> N toggle_output.return_value = False await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: LIGHT_OUTPUT1}, blocking=True, @@ -149,7 +149,7 @@ async def test_output_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> N toggle_output.return_value = True await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: LIGHT_OUTPUT1}, blocking=True, @@ -174,7 +174,7 @@ async def test_relay_turn_on(hass: HomeAssistant, entry: MockConfigEntry) -> Non control_relays.return_value = False await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: LIGHT_RELAY1}, blocking=True, @@ -191,7 +191,7 @@ async def test_relay_turn_on(hass: HomeAssistant, entry: MockConfigEntry) -> Non control_relays.return_value = True await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: LIGHT_RELAY1}, blocking=True, @@ -213,7 +213,7 @@ async def test_relay_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> No states[0] = RelayStateModifier.OFF await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: LIGHT_RELAY1}, blocking=True, @@ -223,7 +223,7 @@ async def test_relay_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> No control_relays.return_value = False await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: LIGHT_RELAY1}, blocking=True, @@ -240,7 +240,7 @@ async def test_relay_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> No control_relays.return_value = True await hass.services.async_call( - DOMAIN_LIGHT, + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: LIGHT_RELAY1}, blocking=True, diff --git a/tests/components/lcn/test_scene.py b/tests/components/lcn/test_scene.py index d26acff1a71d14..0273e8188be065 100644 --- a/tests/components/lcn/test_scene.py +++ b/tests/components/lcn/test_scene.py @@ -5,7 +5,7 @@ from pypck.lcn_defs import OutputPort, RelayPort from syrupy.assertion import SnapshotAssertion -from homeassistant.components.scene import DOMAIN as DOMAIN_SCENE +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, @@ -41,7 +41,7 @@ async def test_scene_activate( await init_integration(hass, entry) with patch.object(MockDeviceConnection, "activate_scene") as activate_scene: await hass.services.async_call( - DOMAIN_SCENE, + SCENE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "scene.testmodule_romantic"}, blocking=True, diff --git a/tests/components/lcn/test_switch.py b/tests/components/lcn/test_switch.py index a7624d28153408..64f729af87c475 100644 --- a/tests/components/lcn/test_switch.py +++ b/tests/components/lcn/test_switch.py @@ -16,7 +16,7 @@ from homeassistant.components.lcn.helpers import get_device_connection from homeassistant.components.lcn.switch import SCAN_INTERVAL -from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -63,7 +63,7 @@ async def test_output_turn_on(hass: HomeAssistant, entry: MockConfigEntry) -> No dim_output.return_value = False await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_OUTPUT1}, blocking=True, @@ -79,7 +79,7 @@ async def test_output_turn_on(hass: HomeAssistant, entry: MockConfigEntry) -> No dim_output.return_value = True await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_OUTPUT1}, blocking=True, @@ -97,7 +97,7 @@ async def test_output_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> N with patch.object(MockDeviceConnection, "dim_output") as dim_output: await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_OUTPUT1}, blocking=True, @@ -107,7 +107,7 @@ async def test_output_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> N dim_output.return_value = False await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: SWITCH_OUTPUT1}, blocking=True, @@ -123,7 +123,7 @@ async def test_output_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> N dim_output.return_value = True await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: SWITCH_OUTPUT1}, blocking=True, @@ -147,7 +147,7 @@ async def test_relay_turn_on(hass: HomeAssistant, entry: MockConfigEntry) -> Non control_relays.return_value = False await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_RELAY1}, blocking=True, @@ -163,7 +163,7 @@ async def test_relay_turn_on(hass: HomeAssistant, entry: MockConfigEntry) -> Non control_relays.return_value = True await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_RELAY1}, blocking=True, @@ -184,7 +184,7 @@ async def test_relay_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> No states[0] = RelayStateModifier.OFF await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_RELAY1}, blocking=True, @@ -194,7 +194,7 @@ async def test_relay_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> No control_relays.return_value = False await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: SWITCH_RELAY1}, blocking=True, @@ -210,7 +210,7 @@ async def test_relay_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> No control_relays.return_value = True await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: SWITCH_RELAY1}, blocking=True, @@ -233,7 +233,7 @@ async def test_regulatorlock_turn_on( lock_regulator.return_value = False await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_REGULATOR1}, blocking=True, @@ -249,7 +249,7 @@ async def test_regulatorlock_turn_on( lock_regulator.return_value = True await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_REGULATOR1}, blocking=True, @@ -269,7 +269,7 @@ async def test_regulatorlock_turn_off( with patch.object(MockDeviceConnection, "lock_regulator") as lock_regulator: await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_REGULATOR1}, blocking=True, @@ -279,7 +279,7 @@ async def test_regulatorlock_turn_off( lock_regulator.return_value = False await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: SWITCH_REGULATOR1}, blocking=True, @@ -295,7 +295,7 @@ async def test_regulatorlock_turn_off( lock_regulator.return_value = True await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: SWITCH_REGULATOR1}, blocking=True, @@ -319,7 +319,7 @@ async def test_keylock_turn_on(hass: HomeAssistant, entry: MockConfigEntry) -> N lock_keys.return_value = False await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_KEYLOCKK1}, blocking=True, @@ -335,7 +335,7 @@ async def test_keylock_turn_on(hass: HomeAssistant, entry: MockConfigEntry) -> N lock_keys.return_value = True await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_KEYLOCKK1}, blocking=True, @@ -356,7 +356,7 @@ async def test_keylock_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> states[0] = KeyLockStateModifier.OFF await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_KEYLOCKK1}, blocking=True, @@ -366,7 +366,7 @@ async def test_keylock_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> lock_keys.return_value = False await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: SWITCH_KEYLOCKK1}, blocking=True, @@ -382,7 +382,7 @@ async def test_keylock_turn_off(hass: HomeAssistant, entry: MockConfigEntry) -> lock_keys.return_value = True await hass.services.async_call( - DOMAIN_SWITCH, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: SWITCH_KEYLOCKK1}, blocking=True, diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 25231c15a3295f..7373118c315325 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -11,7 +11,7 @@ from homeassistant.components import tts from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, ) from homeassistant.core import HomeAssistant @@ -51,7 +51,7 @@ async def test_service_say( hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) config = {tts.DOMAIN: {"platform": "marytts"}} @@ -90,7 +90,7 @@ async def test_service_say_with_effect( hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test service call say with effects.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) config = {tts.DOMAIN: {"platform": "marytts", "effect": {"Volume": "amount:2.0;"}}} @@ -129,7 +129,7 @@ async def test_service_say_http_error( hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) config = {tts.DOMAIN: {"platform": "marytts"}} diff --git a/tests/components/openai_conversation/test_tts.py b/tests/components/openai_conversation/test_tts.py index f1ac2ad111e630..ffbe7117c1e8a4 100644 --- a/tests/components/openai_conversation/test_tts.py +++ b/tests/components/openai_conversation/test_tts.py @@ -12,7 +12,7 @@ from homeassistant.components import tts from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, ) from homeassistant.const import ATTR_ENTITY_ID @@ -38,7 +38,7 @@ def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: @pytest.fixture async def calls(hass: HomeAssistant) -> list[ServiceCall]: """Mock media player calls.""" - return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + return async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) @pytest.fixture(autouse=True) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index dc50f18d5e1912..5a6e988c82d145 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -17,7 +17,7 @@ ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, MediaType, ) @@ -65,7 +65,7 @@ async def test_config_entry_unload( assert state is not None assert state.state == STATE_UNKNOWN - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) now = dt_util.utcnow() freezer.move_to(now) @@ -154,7 +154,7 @@ async def test_service( expected_url_suffix: str, ) -> None: """Set up a TTS platform and call service.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, @@ -217,7 +217,7 @@ async def test_service_default_language( expected_url_suffix: str, ) -> None: """Set up a TTS platform with default language and call service.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, @@ -281,7 +281,7 @@ async def test_service_default_special_language( expected_url_suffix: str, ) -> None: """Set up a TTS platform with default special language and call service.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, @@ -341,7 +341,7 @@ async def test_service_language( expected_url_suffix: str, ) -> None: """Set up a TTS platform and call service with language.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, @@ -401,7 +401,7 @@ async def test_service_wrong_language( expected_url_suffix: str, ) -> None: """Set up a TTS platform and call service.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -455,7 +455,7 @@ async def test_service_options( expected_url_suffix: str, ) -> None: """Set up a TTS platform and call service with options.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, @@ -539,7 +539,7 @@ async def test_service_default_options( expected_url_suffix: str, ) -> None: """Set up a TTS platform and call service with default options.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, @@ -613,7 +613,7 @@ async def test_merge_default_service_options( This tests merging default and user provided options. """ - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, @@ -680,7 +680,7 @@ async def test_service_wrong_options( expected_url_suffix: str, ) -> None: """Set up a TTS platform and call service with wrong options.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -736,7 +736,7 @@ async def test_service_clear_cache( expected_url_suffix: str, ) -> None: """Set up a TTS platform and call service clear cache.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, @@ -798,7 +798,7 @@ async def test_service_receive_voice( expected_url_suffix: str, ) -> None: """Set up a TTS platform and call service and receive voice.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, @@ -870,7 +870,7 @@ async def test_service_receive_voice_german( expected_url_suffix: str, ) -> None: """Set up a TTS platform and call service and receive voice.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, @@ -1001,7 +1001,7 @@ async def test_service_without_cache( expected_url_suffix: str, ) -> None: """Set up a TTS platform with cache and call service without cache.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, @@ -1046,7 +1046,7 @@ async def test_setup_legacy_cache_dir( mock_provider: MockTTSProvider, ) -> None: """Set up a TTS platform with cache and call service without cache.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) tts_data = MOCK_DATA cache_file = ( @@ -1084,7 +1084,7 @@ async def test_setup_cache_dir( mock_tts_entity: MockTTSEntity, ) -> None: """Set up a TTS platform with cache and call service without cache.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) tts_data = MOCK_DATA cache_file = mock_tts_cache_dir / ( @@ -1170,7 +1170,7 @@ async def test_service_get_tts_error( service_data: dict[str, Any], ) -> None: """Set up a TTS platform with wrong get_tts_audio.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) await hass.services.async_call( tts.DOMAIN, diff --git a/tests/components/tts/test_legacy.py b/tests/components/tts/test_legacy.py index 22e8ac35f16171..349e7366461094 100644 --- a/tests/components/tts/test_legacy.py +++ b/tests/components/tts/test_legacy.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components.media_player import ( - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, ) from homeassistant.components.tts import ATTR_MESSAGE, DOMAIN, Provider @@ -146,7 +146,7 @@ async def test_service_without_cache_config( hass: HomeAssistant, mock_tts_cache_dir: Path, mock_tts ) -> None: """Set up a TTS platform without cache.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) config = {DOMAIN: {"platform": "test", "cache": False}} diff --git a/tests/components/tts/test_notify.py b/tests/components/tts/test_notify.py index 00cdae2934f903..8c440a02405876 100644 --- a/tests/components/tts/test_notify.py +++ b/tests/components/tts/test_notify.py @@ -6,7 +6,7 @@ from homeassistant.components import notify, tts from homeassistant.components.media_player import ( - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, ) from homeassistant.core import HomeAssistant @@ -89,7 +89,7 @@ async def test_setup_platform_missing_key(hass: HomeAssistant) -> None: async def test_setup_legacy_service(hass: HomeAssistant) -> None: """Set up the demo platform and call service.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) config = { tts.DOMAIN: {"platform": "demo"}, @@ -130,7 +130,7 @@ async def test_setup_service( hass: HomeAssistant, mock_tts_entity: MockTTSEntity ) -> None: """Set up platform and call service.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) config = { notify.DOMAIN: { diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index e6a30d7fac27f1..48d2aaa31c983b 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -9,7 +9,7 @@ from homeassistant.components import tts from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, ) from homeassistant.core import HomeAssistant @@ -64,7 +64,7 @@ async def test_service_say( aioclient_mock: AiohttpClientMocker, ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) aioclient_mock.post(URL, data=FORM_DATA, status=HTTPStatus.OK, content=b"test") @@ -99,7 +99,7 @@ async def test_service_say_german_config( aioclient_mock: AiohttpClientMocker, ) -> None: """Test service call say with german code in the config.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) form_data = {**FORM_DATA, "hl": "de-de"} aioclient_mock.post(URL, data=form_data, status=HTTPStatus.OK, content=b"test") @@ -141,7 +141,7 @@ async def test_service_say_german_service( aioclient_mock: AiohttpClientMocker, ) -> None: """Test service call say with german code in the service.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) form_data = {**FORM_DATA, "hl": "de-de"} aioclient_mock.post(URL, data=form_data, status=HTTPStatus.OK, content=b"test") @@ -178,7 +178,7 @@ async def test_service_say_error( aioclient_mock: AiohttpClientMocker, ) -> None: """Test service call say with http response 400.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) aioclient_mock.post(URL, data=FORM_DATA, status=400, content=b"test") @@ -212,7 +212,7 @@ async def test_service_say_timeout( aioclient_mock: AiohttpClientMocker, ) -> None: """Test service call say with http timeout.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) aioclient_mock.post(URL, data=FORM_DATA, exc=TimeoutError()) @@ -246,7 +246,7 @@ async def test_service_say_error_msg( aioclient_mock: AiohttpClientMocker, ) -> None: """Test service call say with http error api message.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) aioclient_mock.post( URL, diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 0cc82183bd6814..a0b2943cc1eb29 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -14,9 +14,9 @@ from homeassistant import loader from homeassistant.components.device_automation import toggle_entity -from homeassistant.components.group import DOMAIN as DOMAIN_GROUP +from homeassistant.components.group import DOMAIN as GROUP_DOMAIN from homeassistant.components.light import LightEntityFeature -from homeassistant.components.logger import DOMAIN as DOMAIN_LOGGER +from homeassistant.components.logger import DOMAIN as LOGGER_DOMAIN from homeassistant.components.websocket_api import const from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, @@ -889,16 +889,16 @@ async def test_get_services( assert hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE] is old_cache # Set up an integration that has services and check cache is updated - assert await async_setup_component(hass, DOMAIN_GROUP, {DOMAIN_GROUP: {}}) + assert await async_setup_component(hass, GROUP_DOMAIN, {GROUP_DOMAIN: {}}) await websocket_client.send_json_auto_id({"type": "get_services"}) msg = await websocket_client.receive_json() assert msg == { "id": 3, - "result": {DOMAIN_GROUP: ANY}, + "result": {GROUP_DOMAIN: ANY}, "success": True, "type": "result", } - group_services = msg["result"][DOMAIN_GROUP] + group_services = msg["result"][GROUP_DOMAIN] assert group_services == snapshot assert hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE] is not old_cache @@ -908,7 +908,7 @@ async def test_get_services( msg = await websocket_client.receive_json() assert msg == { "id": 4, - "result": {DOMAIN_GROUP: group_services}, + "result": {GROUP_DOMAIN: group_services}, "success": True, "type": "result", } @@ -944,7 +944,7 @@ def _load_services_file(integration: Integration) -> JSON_TYPE: "set_level": None, } - await async_setup_component(hass, DOMAIN_LOGGER, {DOMAIN_LOGGER: {}}) + await async_setup_component(hass, LOGGER_DOMAIN, {LOGGER_DOMAIN: {}}) await hass.async_block_till_done() with ( @@ -959,13 +959,13 @@ def _load_services_file(integration: Integration) -> JSON_TYPE: assert msg == { "id": 5, "result": { - DOMAIN_LOGGER: ANY, - DOMAIN_GROUP: group_services, + LOGGER_DOMAIN: ANY, + GROUP_DOMAIN: group_services, }, "success": True, "type": "result", } - logger_services = msg["result"][DOMAIN_LOGGER] + logger_services = msg["result"][LOGGER_DOMAIN] assert logger_services == snapshot diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index 098fc025bf367c..5476ec6bae144a 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -9,7 +9,7 @@ from homeassistant.components import tts from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, - DOMAIN as DOMAIN_MP, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, ) from homeassistant.core import HomeAssistant @@ -57,7 +57,7 @@ async def test_service_say( hass_client: ClientSessionGenerator, ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) url_param = { "text": "HomeAssistant", @@ -97,7 +97,7 @@ async def test_service_say_russian_config( hass_client: ClientSessionGenerator, ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) url_param = { "text": "HomeAssistant", @@ -144,7 +144,7 @@ async def test_service_say_russian_service( hass_client: ClientSessionGenerator, ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) url_param = { "text": "HomeAssistant", @@ -188,7 +188,7 @@ async def test_service_say_timeout( hass_client: ClientSessionGenerator, ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) url_param = { "text": "HomeAssistant", @@ -235,7 +235,7 @@ async def test_service_say_http_error( hass_client: ClientSessionGenerator, ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) url_param = { "text": "HomeAssistant", @@ -279,7 +279,7 @@ async def test_service_say_specified_speaker( hass_client: ClientSessionGenerator, ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) url_param = { "text": "HomeAssistant", @@ -325,7 +325,7 @@ async def test_service_say_specified_emotion( hass_client: ClientSessionGenerator, ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) url_param = { "text": "HomeAssistant", @@ -371,7 +371,7 @@ async def test_service_say_specified_low_speed( hass_client: ClientSessionGenerator, ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) url_param = { "text": "HomeAssistant", @@ -413,7 +413,7 @@ async def test_service_say_specified_speed( hass_client: ClientSessionGenerator, ) -> None: """Test service call say.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) url_param = { "text": "HomeAssistant", @@ -453,7 +453,7 @@ async def test_service_say_specified_options( hass_client: ClientSessionGenerator, ) -> None: """Test service call say with options.""" - calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + calls = async_mock_service(hass, MP_DOMAIN, SERVICE_PLAY_MEDIA) url_param = { "text": "HomeAssistant", diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 92702b6f1a3601..41aff9651f8a6f 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -11,12 +11,12 @@ import voluptuous as vol from homeassistant.components.device_automation import ( - DOMAIN as DOMAIN_DEVICE_AUTOMATION, + DOMAIN as DEVICE_AUTOMATION_DOMAIN, ) -from homeassistant.components.light import DOMAIN as DOMAIN_LIGHT +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.components.sun import DOMAIN as DOMAIN_SUN -from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH +from homeassistant.components.sun import DOMAIN as SUN_DOMAIN +from homeassistant.components.system_health import DOMAIN as SYSTEM_HEALTH_DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_CONDITION, @@ -2527,8 +2527,8 @@ async def test_async_get_all_descriptions( ws_client = await hass_ws_client(hass) - assert await async_setup_component(hass, DOMAIN_SUN, {}) - assert await async_setup_component(hass, DOMAIN_SYSTEM_HEALTH, {}) + assert await async_setup_component(hass, SUN_DOMAIN, {}) + assert await async_setup_component(hass, SYSTEM_HEALTH_DOMAIN, {}) await hass.async_block_till_done() def _load_yaml(fname, secrets=None): @@ -2558,7 +2558,7 @@ def _load_yaml(fname, secrets=None): # system_health has no conditions assert proxy_load_conditions_files.mock_calls[0][1][0] == unordered( [ - await async_get_integration(hass, DOMAIN_SUN), + await async_get_integration(hass, SUN_DOMAIN), ] ) @@ -2599,7 +2599,7 @@ def _load_yaml(fname, secrets=None): assert await condition.async_get_all_descriptions(hass) is descriptions # Load the device_automation integration and check a new cache object is created - assert await async_setup_component(hass, DOMAIN_DEVICE_AUTOMATION, {}) + assert await async_setup_component(hass, DEVICE_AUTOMATION_DOMAIN, {}) await hass.async_block_till_done() with ( @@ -2638,7 +2638,7 @@ def _load_yaml(fname, secrets=None): assert await condition.async_get_all_descriptions(hass) is new_descriptions # Load the light integration and check a new cache object is created - assert await async_setup_component(hass, DOMAIN_LIGHT, {}) + assert await async_setup_component(hass, LIGHT_DOMAIN, {}) await hass.async_block_till_done() with ( @@ -2765,7 +2765,7 @@ async def test_async_get_all_descriptions_with_yaml_error( expected_message: str, ) -> None: """Test async_get_all_descriptions.""" - assert await async_setup_component(hass, DOMAIN_SUN, {}) + assert await async_setup_component(hass, SUN_DOMAIN, {}) await hass.async_block_till_done() def _load_yaml_dict(fname, secrets=None): @@ -2780,7 +2780,7 @@ def _load_yaml_dict(fname, secrets=None): ): descriptions = await condition.async_get_all_descriptions(hass) - assert descriptions == {DOMAIN_SUN: None} + assert descriptions == {SUN_DOMAIN: None} assert expected_message in caplog.text @@ -2795,7 +2795,7 @@ async def test_async_get_all_descriptions_with_bad_description( fields: not_a_dict """ - assert await async_setup_component(hass, DOMAIN_SUN, {}) + assert await async_setup_component(hass, SUN_DOMAIN, {}) await hass.async_block_till_done() def _load_yaml(fname, secrets=None): diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 9e798cccade09a..5ebc6a51721b0c 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -17,11 +17,11 @@ from homeassistant import config_entries, exceptions from homeassistant.auth.permissions import PolicyPermissions import homeassistant.components # noqa: F401 -from homeassistant.components.group import DOMAIN as DOMAIN_GROUP, Group -from homeassistant.components.input_button import DOMAIN as DOMAIN_INPUT_BUTTON -from homeassistant.components.logger import DOMAIN as DOMAIN_LOGGER -from homeassistant.components.shell_command import DOMAIN as DOMAIN_SHELL_COMMAND -from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH +from homeassistant.components.group import DOMAIN as GROUP_DOMAIN, Group +from homeassistant.components.input_button import DOMAIN as INPUT_BUTTON_DOMAIN +from homeassistant.components.logger import DOMAIN as LOGGER_DOMAIN +from homeassistant.components.shell_command import DOMAIN as SHELL_COMMAND_DOMAIN +from homeassistant.components.system_health import DOMAIN as SYSTEM_HEALTH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, @@ -853,9 +853,9 @@ async def test_extract_entity_ids_from_labels(hass: HomeAssistant) -> None: async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: """Test async_get_all_descriptions.""" - group_config = {DOMAIN_GROUP: {}} - assert await async_setup_component(hass, DOMAIN_GROUP, group_config) - assert await async_setup_component(hass, DOMAIN_SYSTEM_HEALTH, {}) + group_config = {GROUP_DOMAIN: {}} + assert await async_setup_component(hass, GROUP_DOMAIN, group_config) + assert await async_setup_component(hass, SYSTEM_HEALTH_DOMAIN, {}) with patch( "homeassistant.helpers.service._load_services_files", @@ -867,19 +867,19 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: # And system_health has no services assert proxy_load_services_files.mock_calls[0][1][0] == unordered( [ - await async_get_integration(hass, DOMAIN_GROUP), + await async_get_integration(hass, GROUP_DOMAIN), ] ) assert len(descriptions) == 1 - assert DOMAIN_GROUP in descriptions - assert "description" not in descriptions[DOMAIN_GROUP]["reload"] - assert "fields" in descriptions[DOMAIN_GROUP]["reload"] + assert GROUP_DOMAIN in descriptions + assert "description" not in descriptions[GROUP_DOMAIN]["reload"] + assert "fields" in descriptions[GROUP_DOMAIN]["reload"] # Does not have services - assert DOMAIN_SYSTEM_HEALTH not in descriptions + assert SYSTEM_HEALTH_DOMAIN not in descriptions - logger_config = {DOMAIN_LOGGER: {}} + logger_config = {LOGGER_DOMAIN: {}} # Test legacy service with translations in services.yaml def _load_services_file(integration: Integration) -> JSON_TYPE: @@ -915,58 +915,58 @@ def _load_services_file(integration: Integration) -> JSON_TYPE: "homeassistant.helpers.service._load_services_file", side_effect=_load_services_file, ): - await async_setup_component(hass, DOMAIN_LOGGER, logger_config) + await async_setup_component(hass, LOGGER_DOMAIN, logger_config) descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 2 - assert DOMAIN_LOGGER in descriptions - assert descriptions[DOMAIN_LOGGER]["set_default_level"]["name"] == "Translated name" + assert LOGGER_DOMAIN in descriptions + assert descriptions[LOGGER_DOMAIN]["set_default_level"]["name"] == "Translated name" assert ( - descriptions[DOMAIN_LOGGER]["set_default_level"]["description"] + descriptions[LOGGER_DOMAIN]["set_default_level"]["description"] == "Translated description" ) assert ( - descriptions[DOMAIN_LOGGER]["set_default_level"]["fields"]["level"]["name"] + descriptions[LOGGER_DOMAIN]["set_default_level"]["fields"]["level"]["name"] == "Field name" ) assert ( - descriptions[DOMAIN_LOGGER]["set_default_level"]["fields"]["level"][ + descriptions[LOGGER_DOMAIN]["set_default_level"]["fields"]["level"][ "description" ] == "Field description" ) assert ( - descriptions[DOMAIN_LOGGER]["set_default_level"]["fields"]["level"]["example"] + descriptions[LOGGER_DOMAIN]["set_default_level"]["fields"]["level"]["example"] == "Field example" ) - hass.services.async_register(DOMAIN_LOGGER, "new_service", lambda x: None, None) + hass.services.async_register(LOGGER_DOMAIN, "new_service", lambda x: None, None) service.async_set_service_schema( - hass, DOMAIN_LOGGER, "new_service", {"description": "new service"} + hass, LOGGER_DOMAIN, "new_service", {"description": "new service"} ) descriptions = await service.async_get_all_descriptions(hass) - assert "description" in descriptions[DOMAIN_LOGGER]["new_service"] - assert descriptions[DOMAIN_LOGGER]["new_service"]["description"] == "new service" + assert "description" in descriptions[LOGGER_DOMAIN]["new_service"] + assert descriptions[LOGGER_DOMAIN]["new_service"]["description"] == "new service" hass.services.async_register( - DOMAIN_LOGGER, "another_new_service", lambda x: None, None + LOGGER_DOMAIN, "another_new_service", lambda x: None, None ) hass.services.async_register( - DOMAIN_LOGGER, + LOGGER_DOMAIN, "service_with_optional_response", lambda x: None, None, SupportsResponse.OPTIONAL, ) hass.services.async_register( - DOMAIN_LOGGER, + LOGGER_DOMAIN, "service_with_only_response", lambda x: None, None, SupportsResponse.ONLY, ) hass.services.async_register( - DOMAIN_LOGGER, + LOGGER_DOMAIN, "another_service_with_response", lambda x: None, None, @@ -974,22 +974,22 @@ def _load_services_file(integration: Integration) -> JSON_TYPE: ) service.async_set_service_schema( hass, - DOMAIN_LOGGER, + LOGGER_DOMAIN, "another_service_with_response", {"description": "response service"}, ) descriptions = await service.async_get_all_descriptions(hass) - assert "another_new_service" in descriptions[DOMAIN_LOGGER] - assert "service_with_optional_response" in descriptions[DOMAIN_LOGGER] - assert descriptions[DOMAIN_LOGGER]["service_with_optional_response"][ + assert "another_new_service" in descriptions[LOGGER_DOMAIN] + assert "service_with_optional_response" in descriptions[LOGGER_DOMAIN] + assert descriptions[LOGGER_DOMAIN]["service_with_optional_response"][ "response" ] == {"optional": True} - assert "service_with_only_response" in descriptions[DOMAIN_LOGGER] - assert descriptions[DOMAIN_LOGGER]["service_with_only_response"]["response"] == { + assert "service_with_only_response" in descriptions[LOGGER_DOMAIN] + assert descriptions[LOGGER_DOMAIN]["service_with_only_response"]["response"] == { "optional": False } - assert "another_service_with_response" in descriptions[DOMAIN_LOGGER] - assert descriptions[DOMAIN_LOGGER]["another_service_with_response"]["response"] == { + assert "another_service_with_response" in descriptions[LOGGER_DOMAIN] + assert descriptions[LOGGER_DOMAIN]["another_service_with_response"]["response"] == { "optional": True } @@ -1207,20 +1207,20 @@ async def test_async_get_all_descriptions_failing_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test async_get_all_descriptions when async_get_integrations returns an exception.""" - group_config = {DOMAIN_GROUP: {}} - await async_setup_component(hass, DOMAIN_GROUP, group_config) + group_config = {GROUP_DOMAIN: {}} + await async_setup_component(hass, GROUP_DOMAIN, group_config) - logger_config = {DOMAIN_LOGGER: {}} - await async_setup_component(hass, DOMAIN_LOGGER, logger_config) + logger_config = {LOGGER_DOMAIN: {}} + await async_setup_component(hass, LOGGER_DOMAIN, logger_config) - input_button_config = {DOMAIN_INPUT_BUTTON: {}} - await async_setup_component(hass, DOMAIN_INPUT_BUTTON, input_button_config) + input_button_config = {INPUT_BUTTON_DOMAIN: {}} + await async_setup_component(hass, INPUT_BUTTON_DOMAIN, input_button_config) async def wrap_get_integrations( hass: HomeAssistant, domains: Iterable[str] ) -> dict[str, Integration | Exception]: integrations = await async_get_integrations(hass, domains) - integrations[DOMAIN_LOGGER] = ImportError("Failed to load services.yaml") + integrations[LOGGER_DOMAIN] = ImportError("Failed to load services.yaml") return integrations with ( @@ -1236,35 +1236,35 @@ async def wrap_get_integrations( # Services are empty defaults if the load fails but should # not raise - assert "description" not in descriptions[DOMAIN_GROUP]["remove"] - assert descriptions[DOMAIN_GROUP]["remove"]["fields"] + assert "description" not in descriptions[GROUP_DOMAIN]["remove"] + assert descriptions[GROUP_DOMAIN]["remove"]["fields"] - assert descriptions[DOMAIN_LOGGER]["set_level"] == {"fields": {}} + assert descriptions[LOGGER_DOMAIN]["set_level"] == {"fields": {}} - assert "description" not in descriptions[DOMAIN_INPUT_BUTTON]["press"] - assert descriptions[DOMAIN_INPUT_BUTTON]["press"]["fields"] == {} - assert "target" in descriptions[DOMAIN_INPUT_BUTTON]["press"] + assert "description" not in descriptions[INPUT_BUTTON_DOMAIN]["press"] + assert descriptions[INPUT_BUTTON_DOMAIN]["press"]["fields"] == {} + assert "target" in descriptions[INPUT_BUTTON_DOMAIN]["press"] - hass.services.async_register(DOMAIN_LOGGER, "new_service", lambda x: None, None) + hass.services.async_register(LOGGER_DOMAIN, "new_service", lambda x: None, None) service.async_set_service_schema( - hass, DOMAIN_LOGGER, "new_service", {"description": "new service"} + hass, LOGGER_DOMAIN, "new_service", {"description": "new service"} ) descriptions = await service.async_get_all_descriptions(hass) - assert "description" in descriptions[DOMAIN_LOGGER]["new_service"] - assert descriptions[DOMAIN_LOGGER]["new_service"]["description"] == "new service" + assert "description" in descriptions[LOGGER_DOMAIN]["new_service"] + assert descriptions[LOGGER_DOMAIN]["new_service"]["description"] == "new service" hass.services.async_register( - DOMAIN_LOGGER, "another_new_service", lambda x: None, None + LOGGER_DOMAIN, "another_new_service", lambda x: None, None ) hass.services.async_register( - DOMAIN_LOGGER, + LOGGER_DOMAIN, "service_with_optional_response", lambda x: None, None, SupportsResponse.OPTIONAL, ) hass.services.async_register( - DOMAIN_LOGGER, + LOGGER_DOMAIN, "service_with_only_response", lambda x: None, None, @@ -1272,13 +1272,13 @@ async def wrap_get_integrations( ) descriptions = await service.async_get_all_descriptions(hass) - assert "another_new_service" in descriptions[DOMAIN_LOGGER] - assert "service_with_optional_response" in descriptions[DOMAIN_LOGGER] - assert descriptions[DOMAIN_LOGGER]["service_with_optional_response"][ + assert "another_new_service" in descriptions[LOGGER_DOMAIN] + assert "service_with_optional_response" in descriptions[LOGGER_DOMAIN] + assert descriptions[LOGGER_DOMAIN]["service_with_optional_response"][ "response" ] == {"optional": True} - assert "service_with_only_response" in descriptions[DOMAIN_LOGGER] - assert descriptions[DOMAIN_LOGGER]["service_with_only_response"]["response"] == { + assert "service_with_only_response" in descriptions[LOGGER_DOMAIN] + assert descriptions[LOGGER_DOMAIN]["service_with_only_response"]["response"] == { "optional": False } @@ -1290,8 +1290,8 @@ async def test_async_get_all_descriptions_dynamically_created_services( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test async_get_all_descriptions when async_get_integrations when services are dynamic.""" - group_config = {DOMAIN_GROUP: {}} - await async_setup_component(hass, DOMAIN_GROUP, group_config) + group_config = {GROUP_DOMAIN: {}} + await async_setup_component(hass, GROUP_DOMAIN, group_config) descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 1 @@ -1299,12 +1299,12 @@ async def test_async_get_all_descriptions_dynamically_created_services( assert "description" not in descriptions["group"]["reload"] assert "fields" in descriptions["group"]["reload"] - shell_command_config = {DOMAIN_SHELL_COMMAND: {"test_service": "ls /bin"}} - await async_setup_component(hass, DOMAIN_SHELL_COMMAND, shell_command_config) + shell_command_config = {SHELL_COMMAND_DOMAIN: {"test_service": "ls /bin"}} + await async_setup_component(hass, SHELL_COMMAND_DOMAIN, shell_command_config) descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 2 - assert descriptions[DOMAIN_SHELL_COMMAND]["test_service"] == { + assert descriptions[SHELL_COMMAND_DOMAIN]["test_service"] == { "fields": {}, "response": {"optional": True}, } @@ -1314,8 +1314,8 @@ async def test_async_get_all_descriptions_new_service_added_while_loading( hass: HomeAssistant, ) -> None: """Test async_get_all_descriptions when a new service is added while loading translations.""" - group_config = {DOMAIN_GROUP: {}} - await async_setup_component(hass, DOMAIN_GROUP, group_config) + group_config = {GROUP_DOMAIN: {}} + await async_setup_component(hass, GROUP_DOMAIN, group_config) descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 1 @@ -1323,7 +1323,7 @@ async def test_async_get_all_descriptions_new_service_added_while_loading( assert "description" not in descriptions["group"]["reload"] assert "fields" in descriptions["group"]["reload"] - logger_domain = DOMAIN_LOGGER + logger_domain = LOGGER_DOMAIN logger_config = {logger_domain: {}} translations_called = threading.Event() @@ -1494,8 +1494,8 @@ async def test_register_with_mixed_case(hass: HomeAssistant) -> None: For backwards compatibility, we have historically allowed mixed case, and automatically converted it to lowercase. """ - logger_config = {DOMAIN_LOGGER: {}} - await async_setup_component(hass, DOMAIN_LOGGER, logger_config) + logger_config = {LOGGER_DOMAIN: {}} + await async_setup_component(hass, LOGGER_DOMAIN, logger_config) logger_domain_mixed = "LoGgEr" hass.services.async_register( logger_domain_mixed, "NeW_SeRVICE", lambda x: None, None @@ -1504,8 +1504,8 @@ async def test_register_with_mixed_case(hass: HomeAssistant) -> None: hass, logger_domain_mixed, "NeW_SeRVICE", {"description": "new service"} ) descriptions = await service.async_get_all_descriptions(hass) - assert "description" in descriptions[DOMAIN_LOGGER]["new_service"] - assert descriptions[DOMAIN_LOGGER]["new_service"]["description"] == "new service" + assert "description" in descriptions[LOGGER_DOMAIN]["new_service"] + assert descriptions[LOGGER_DOMAIN]["new_service"]["description"] == "new service" async def test_call_with_required_features(hass: HomeAssistant, mock_entities) -> None: diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index 092ae05d96aa96..21ed040af246cb 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -10,10 +10,10 @@ import voluptuous as vol from homeassistant.components import automation -from homeassistant.components.sun import DOMAIN as DOMAIN_SUN -from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH -from homeassistant.components.tag import DOMAIN as DOMAIN_TAG -from homeassistant.components.text import DOMAIN as DOMAIN_TEXT +from homeassistant.components.sun import DOMAIN as SUN_DOMAIN +from homeassistant.components.system_health import DOMAIN as SYSTEM_HEALTH_DOMAIN +from homeassistant.components.tag import DOMAIN as TAG_DOMAIN +from homeassistant.components.text import DOMAIN as TEXT_DOMAIN from homeassistant.const import ( CONF_ABOVE, CONF_BELOW, @@ -719,8 +719,8 @@ async def test_async_get_all_descriptions( ws_client = await hass_ws_client(hass) - assert await async_setup_component(hass, DOMAIN_SUN, {}) - assert await async_setup_component(hass, DOMAIN_SYSTEM_HEALTH, {}) + assert await async_setup_component(hass, SUN_DOMAIN, {}) + assert await async_setup_component(hass, SYSTEM_HEALTH_DOMAIN, {}) await hass.async_block_till_done() def _load_yaml(fname, secrets=None): @@ -750,7 +750,7 @@ def _load_yaml(fname, secrets=None): # system_health has no triggers assert proxy_load_triggers_files.mock_calls[0][1][0] == unordered( [ - await async_get_integration(hass, DOMAIN_SUN), + await async_get_integration(hass, SUN_DOMAIN), ] ) @@ -780,7 +780,7 @@ def _load_yaml(fname, secrets=None): assert await trigger.async_get_all_descriptions(hass) is descriptions # Load the tag integration and check a new cache object is created - assert await async_setup_component(hass, DOMAIN_TAG, {}) + assert await async_setup_component(hass, TAG_DOMAIN, {}) await hass.async_block_till_done() with ( @@ -811,7 +811,7 @@ def _load_yaml(fname, secrets=None): assert await trigger.async_get_all_descriptions(hass) is new_descriptions # Load the text integration and check a new cache object is created - assert await async_setup_component(hass, DOMAIN_TEXT, {}) + assert await async_setup_component(hass, TEXT_DOMAIN, {}) await hass.async_block_till_done() with ( @@ -926,7 +926,7 @@ async def test_async_get_all_descriptions_with_yaml_error( expected_message: str, ) -> None: """Test async_get_all_descriptions.""" - assert await async_setup_component(hass, DOMAIN_SUN, {}) + assert await async_setup_component(hass, SUN_DOMAIN, {}) await hass.async_block_till_done() def _load_yaml_dict(fname, secrets=None): @@ -941,7 +941,7 @@ def _load_yaml_dict(fname, secrets=None): ): descriptions = await trigger.async_get_all_descriptions(hass) - assert descriptions == {DOMAIN_SUN: None} + assert descriptions == {SUN_DOMAIN: None} assert expected_message in caplog.text @@ -956,7 +956,7 @@ async def test_async_get_all_descriptions_with_bad_description( fields: not_a_dict """ - assert await async_setup_component(hass, DOMAIN_SUN, {}) + assert await async_setup_component(hass, SUN_DOMAIN, {}) await hass.async_block_till_done() def _load_yaml(fname, secrets=None): @@ -972,7 +972,7 @@ def _load_yaml(fname, secrets=None): ): descriptions = await trigger.async_get_all_descriptions(hass) - assert descriptions == {DOMAIN_SUN: None} + assert descriptions == {SUN_DOMAIN: None} assert ( "Unable to parse triggers.yaml for the sun integration: " From d61f7d81709a66bfce65ecc2e3a9870bc73479a8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:39:16 +0100 Subject: [PATCH 24/36] Use shorthand attributes in geo_rss_events (#163268) --- .../components/geo_rss_events/sensor.py | 43 ++++--------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index 079a47a6c27a18..34f5283b50c917 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -40,7 +40,6 @@ CONF_CATEGORIES = "categories" -DEFAULT_ICON = "mdi:alert" DEFAULT_NAME = "Event Service" DEFAULT_RADIUS_IN_KM = 20.0 DEFAULT_UNIT_OF_MEASUREMENT = "Events" @@ -111,15 +110,14 @@ def setup_platform( class GeoRssServiceSensor(SensorEntity): """Representation of a Sensor.""" + _attr_icon = "mdi:alert" + def __init__( self, coordinates, url, radius, category, service_name, unit_of_measurement ): """Initialize the sensor.""" - self._category = category - self._service_name = service_name - self._state = None - self._state_attributes = None - self._unit_of_measurement = unit_of_measurement + self._attr_name = f"{service_name} {'Any' if category is None else category}" + self._attr_native_unit_of_measurement = unit_of_measurement self._feed = GenericFeed( coordinates, @@ -128,31 +126,6 @@ def __init__( filter_categories=None if not category else [category], ) - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._service_name} {'Any' if self._category is None else self._category}" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @property - def icon(self): - """Return the default icon to use in the frontend.""" - return DEFAULT_ICON - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._state_attributes - def update(self) -> None: """Update this sensor from the GeoRSS service.""" @@ -161,14 +134,14 @@ def update(self) -> None: _LOGGER.debug( "Adding events to sensor %s: %s", self.entity_id, feed_entries ) - self._state = len(feed_entries) + self._attr_native_value = len(feed_entries) # And now compute the attributes from the filtered events. matrix = {} for entry in feed_entries: matrix[entry.title] = ( f"{entry.distance_to_home:.0f}{UnitOfLength.KILOMETERS}" ) - self._state_attributes = matrix + self._attr_extra_state_attributes = matrix elif status == UPDATE_OK_NO_DATA: _LOGGER.debug("Update successful, but no data received from %s", self._feed) # Don't change the state or state attributes. @@ -178,5 +151,5 @@ def update(self) -> None: ) # If no events were found due to an error then just set state to # zero. - self._state = 0 - self._state_attributes = {} + self._attr_native_value = 0 + self._attr_extra_state_attributes = {} From 59dad4c935ab3fbb6322bfcaf706e852107b8779 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 17 Feb 2026 15:15:42 +0100 Subject: [PATCH 25/36] Add DHCP Discovery for SmartThings (#160314) --- .../components/smartthings/manifest.json | 3 + .../components/smartthings/strings.json | 3 + homeassistant/generated/dhcp.py | 4 + .../smartthings/test_config_flow.py | 89 ++++++++++++++++++- 4 files changed, 98 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 17aababd641af7..a30bcaee30a163 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -24,6 +24,9 @@ { "hostname": "hub*", "macaddress": "286D97*" + }, + { + "hostname": "samsung-*" } ], "documentation": "https://www.home-assistant.io/integrations/smartthings", diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 6a1d69ec866243..625f878625992d 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -23,6 +23,9 @@ "user": "[%key:common::config_flow::initiate_flow::account%]" }, "step": { + "oauth_discovery": { + "description": "Home Assistant has found a SmartThings device on your network. Press **Submit** to continue setting up SmartThings." + }, "pick_implementation": { "data": { "implementation": "[%key:common::config_flow::data::implementation%]" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index e650435a2e0e86..d3dde435250cf9 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -813,6 +813,10 @@ "hostname": "hub*", "macaddress": "286D97*", }, + { + "domain": "smartthings", + "hostname": "samsung-*", + }, { "domain": "smlight", "registered_devices": True, diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 02011ac897b5e8..253e0ec27937eb 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -11,11 +11,12 @@ CONF_SUBSCRIPTION_ID, DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -614,3 +615,89 @@ async def test_migration_no_cloud( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cloud_not_enabled" + + +@pytest.mark.usefixtures("current_request_with_host", "use_cloud") +async def test_dhcp_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_smartthings: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Check a full flow initiated by DHCP discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + ip="192.168.0.2", hostname="Samsung-Washer", macaddress="88571dc3ed7d" + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "oauth_discovery" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] is FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + "https://api.smartthings.com/oauth/authorize" + "?response_type=code&client_id=CLIENT_ID" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=r:devices:*+w:devices:*+x:devices:*+r:hubs:*+" + "r:locations:*+w:locations:*+x:locations:*+r:scenes:*+" + "x:scenes:*+r:rules:*+w:rules:*+sse+r:installedapps+" + "w:installedapps" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + "https://auth-global.api.smartthings.com/oauth/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "token_type": "Bearer", + "expires_in": 82806, + "scope": "r:devices:* w:devices:* x:devices:* r:hubs:* " + "r:locations:* w:locations:* x:locations:* " + "r:scenes:* x:scenes:* r:rules:* w:rules:* sse", + "access_tier": 0, + "installed_app_id": "5aaaa925-2be1-4e40-b257-e4ef59083324", + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +@pytest.mark.usefixtures("current_request_with_host", "use_cloud") +async def test_duplicate_entry_dhcp( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test duplicate entry is not able to set up.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + ip="192.168.0.2", hostname="Samsung-Washer", macaddress="88571dc3ed7d" + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" From c205785f4f43ef185ea32f93c29c28e657ffa18c Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Tue, 17 Feb 2026 17:20:57 +0300 Subject: [PATCH 26/36] Add quality scale to Anthropic (#162953) --- .../components/anthropic/quality_scale.yaml | 119 ++++++++++++++++++ script/hassfest/quality_scale.py | 1 - 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/anthropic/quality_scale.yaml diff --git a/homeassistant/components/anthropic/quality_scale.yaml b/homeassistant/components/anthropic/quality_scale.yaml new file mode 100644 index 00000000000000..351e2e88afa5ae --- /dev/null +++ b/homeassistant/components/anthropic/quality_scale.yaml @@ -0,0 +1,119 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: | + Integration has no actions. + appropriate-polling: + status: exempt + comment: | + Integration does not poll. + brands: done + common-modules: done + config-flow-test-coverage: + status: todo + comment: | + * Remove integration setup from the config flow init test + * Make `mock_setup_entry` a separate fixture + * Use the mock_config_entry fixture in `test_duplicate_entry` + * `test_duplicate_entry`: Patch `homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.list` + * Fix docstring and name for `test_form_invalid_auth` (does not only test auth) + * In `test_form_invalid_auth`, make sure the test run until CREATE_ENTRY to test that the flow is able to recover + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: | + Integration has no actions. + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: | + Integration does not subscribe to events. + entity-unique-id: done + has-entity-name: done + runtime-data: + status: todo + comment: | + To redesign deferred reloading. + test-before-configure: done + test-before-setup: done + unique-config-entry: done + # Silver + action-exceptions: + status: todo + comment: | + Reevaluate exceptions for entity services. + config-entry-unloading: done + docs-configuration-parameters: done + docs-installation-parameters: done + entity-unavailable: todo + integration-owner: done + log-when-unavailable: todo + parallel-updates: + status: exempt + comment: | + The API does not limit parallel updates. + reauthentication-flow: done + test-coverage: done + # Gold + devices: done + diagnostics: todo + discovery-update-info: + status: exempt + comment: | + Service integration, no discovery. + discovery: + status: exempt + comment: | + Service integration, no discovery. + docs-data-update: + status: exempt + comment: | + No data updates. + docs-examples: + status: todo + comment: | + To give examples of how people use the integration + docs-known-limitations: done + docs-supported-devices: + status: todo + comment: | + To write something about what models we support. + docs-supported-functions: done + docs-troubleshooting: todo + docs-use-cases: done + dynamic-devices: + status: exempt + comment: | + Service integration, no devices. + entity-category: + status: exempt + comment: | + No entities with categories. + entity-device-class: + status: exempt + comment: | + No entities with device classes. + entity-disabled-by-default: + status: exempt + comment: | + No entities disabled by default. + entity-translations: todo + exception-translations: todo + icon-translations: todo + reconfiguration-flow: done + repair-issues: done + stale-devices: + status: exempt + comment: | + Service integration, no devices. + # Platinum + async-dependency: done + inject-websession: + status: done + comment: | + Uses `httpx` session. + strict-typing: done diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 1090de74dcadcd..013f80a165f5aa 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -155,7 +155,6 @@ class Rule: "anel_pwrctrl", "anova", "anthemav", - "anthropic", "aosmith", "apache_kafka", "apple_tv", From ff2f0ac32078ffac0f7ad43875ea173c7471834d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:34:16 +0100 Subject: [PATCH 27/36] Mark RestoreEntity/RestoreSensor type hints as mandatory (#163272) --- pylint/plugins/hass_enforce_type_hints.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 1c422c6cc452d5..da31c415828d03 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -781,14 +781,17 @@ class ClassTypeHintMatch: TypeHintMatch( function_name="async_get_last_state", return_type=["State", None], + mandatory=True, ), TypeHintMatch( function_name="async_get_last_extra_data", return_type=["ExtraStoredData", None], + mandatory=True, ), TypeHintMatch( function_name="extra_restore_state_data", return_type=["ExtraStoredData", None], + mandatory=True, ), ] _TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ @@ -2559,10 +2562,12 @@ class ClassTypeHintMatch: TypeHintMatch( function_name="extra_restore_state_data", return_type="SensorExtraStoredData", + mandatory=True, ), TypeHintMatch( function_name="async_get_last_sensor_data", return_type=["SensorExtraStoredData", None], + mandatory=True, ), ], ), From 91c36fcdf68ecc1f8900b4fa111183c9cca967ac Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:47:56 +0100 Subject: [PATCH 28/36] Fix dynamic entity creation in eheimdigital (#161155) --- .../components/eheimdigital/coordinator.py | 18 ++++++- tests/components/eheimdigital/test_init.py | 52 ++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eheimdigital/coordinator.py b/homeassistant/components/eheimdigital/coordinator.py index df5475b6567aaf..61c3be363c8396 100644 --- a/homeassistant/components/eheimdigital/coordinator.py +++ b/homeassistant/components/eheimdigital/coordinator.py @@ -53,6 +53,7 @@ def __init__( main_device_added_event=self.main_device_added_event, ) self.known_devices: set[str] = set() + self.incomplete_devices: set[str] = set() self.platform_callbacks: set[AsyncSetupDeviceEntitiesCallback] = set() def add_platform_callback( @@ -70,11 +71,26 @@ async def _async_device_found( This function is called from the library whenever a new device is added. """ - if device_address not in self.known_devices: + if self.hub.devices[device_address].is_missing_data: + self.incomplete_devices.add(device_address) + return + + if ( + device_address not in self.known_devices + or device_address in self.incomplete_devices + ): for platform_callback in self.platform_callbacks: platform_callback({device_address: self.hub.devices[device_address]}) + if device_address in self.incomplete_devices: + self.incomplete_devices.remove(device_address) async def _async_receive_callback(self) -> None: + if any(self.incomplete_devices): + for device_address in self.incomplete_devices.copy(): + if not self.hub.devices[device_address].is_missing_data: + await self._async_device_found( + device_address, EheimDeviceType.VERSION_UNDEFINED + ) self.async_set_updated_data(self.hub.devices) async def _async_setup(self) -> None: diff --git a/tests/components/eheimdigital/test_init.py b/tests/components/eheimdigital/test_init.py index 4b2823389549e3..8087c0a927ef6d 100644 --- a/tests/components/eheimdigital/test_init.py +++ b/tests/components/eheimdigital/test_init.py @@ -1,12 +1,14 @@ """Tests for the init module.""" -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from eheimdigital.types import EheimDeviceType, EheimDigitalClientError +from homeassistant.components.eheimdigital.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from .conftest import init_integration @@ -15,6 +17,52 @@ from tests.typing import WebSocketGenerator +async def test_dynamic_entities( + hass: HomeAssistant, + eheimdigital_hub_mock: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test dynamic adding of entities.""" + mock_config_entry.add_to_hass(hass) + heater_data = eheimdigital_hub_mock.return_value.devices[ + "00:00:00:00:00:02" + ].heater_data + eheimdigital_hub_mock.return_value.devices["00:00:00:00:00:02"].heater_data = None + with ( + patch( + "homeassistant.components.eheimdigital.coordinator.asyncio.Event", + new=AsyncMock, + ), + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + for device in eheimdigital_hub_mock.return_value.devices: + await eheimdigital_hub_mock.call_args.kwargs["device_found_callback"]( + device, eheimdigital_hub_mock.return_value.devices[device].device_type + ) + await hass.async_block_till_done() + + assert ( + entity_registry.async_get_entity_id( + DOMAIN, Platform.NUMBER, "mock_heater_night_temperature_offset" + ) + is None + ) + + eheimdigital_hub_mock.return_value.devices[ + "00:00:00:00:00:02" + ].heater_data = heater_data + + await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() + + assert hass.states.get("number.mock_heater_night_temperature_offset").state == str( + eheimdigital_hub_mock.return_value.devices[ + "00:00:00:00:00:02" + ].night_temperature_offset + ) + + async def test_remove_device( hass: HomeAssistant, eheimdigital_hub_mock: MagicMock, From b23c402d0ae4231fc754737bfa909e69a1c2a57e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:48:14 +0100 Subject: [PATCH 29/36] Improve haveibeenpwned type hints (#163280) --- .../components/haveibeenpwned/sensor.py | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index d9d2889848e8f1..0e8de64d7c61b4 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -5,6 +5,7 @@ from datetime import timedelta from http import HTTPStatus import logging +from typing import TYPE_CHECKING, Any import requests import voluptuous as vol @@ -59,40 +60,26 @@ class HaveIBeenPwnedSensor(SensorEntity): _attr_attribution = "Data provided by Have I Been Pwned (HIBP)" - def __init__(self, data, email): + def __init__(self, data: HaveIBeenPwnedData, email: str) -> None: """Initialize the HaveIBeenPwned sensor.""" - self._state = None self._data = data self._email = email - self._unit_of_measurement = "Breaches" + self._attr_name = f"Breaches {email}" + self._attr_native_unit_of_measurement = "Breaches" @property - def name(self): - """Return the name of the sensor.""" - return f"Breaches {self._email}" - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def native_value(self): - """Return the state of the device.""" - return self._state - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes of the sensor.""" - val = {} + val: dict[str, Any] = {} if self._email not in self._data.data: return val for idx, value in enumerate(self._data.data[self._email]): tmpname = f"breach {idx + 1}" - datetime_local = dt_util.as_local( - dt_util.parse_datetime(value["AddedDate"]) - ) + parsed_datetime = dt_util.parse_datetime(value["AddedDate"]) + if TYPE_CHECKING: + assert parsed_datetime is not None + datetime_local = dt_util.as_local(parsed_datetime) tmpvalue = f"{value['Title']} {datetime_local.strftime(DATE_STR_FORMAT)}" val[tmpname] = tmpvalue @@ -121,7 +108,7 @@ def update_nothrottle(self, dummy=None): ) return - self._state = len(self._data.data[self._email]) + self._attr_native_value = len(self._data.data[self._email]) self.schedule_update_ha_state() def update(self) -> None: @@ -129,7 +116,7 @@ def update(self) -> None: self._data.update() if self._email in self._data.data: - self._state = len(self._data.data[self._email]) + self._attr_native_value = len(self._data.data[self._email]) class HaveIBeenPwnedData: From f6f52005fe002d76a0541f6d57ff54eac02551a4 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 17 Feb 2026 15:51:55 +0100 Subject: [PATCH 30/36] Add Indevolt integration (#160595) Co-authored-by: Joostlek --- CODEOWNERS | 2 + homeassistant/components/indevolt/__init__.py | 28 + .../components/indevolt/config_flow.py | 83 + homeassistant/components/indevolt/const.py | 103 + .../components/indevolt/coordinator.py | 80 + homeassistant/components/indevolt/entity.py | 31 + .../components/indevolt/manifest.json | 11 + .../components/indevolt/quality_scale.yaml | 94 + homeassistant/components/indevolt/sensor.py | 704 +++ .../components/indevolt/strings.json | 246 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/indevolt/__init__.py | 14 + tests/components/indevolt/conftest.py | 108 + tests/components/indevolt/fixtures/gen_1.json | 23 + tests/components/indevolt/fixtures/gen_2.json | 74 + .../indevolt/snapshots/test_sensor.ambr | 4463 +++++++++++++++++ tests/components/indevolt/test_config_flow.py | 106 + tests/components/indevolt/test_init.py | 47 + tests/components/indevolt/test_sensor.py | 164 + 22 files changed, 6394 insertions(+) create mode 100644 homeassistant/components/indevolt/__init__.py create mode 100644 homeassistant/components/indevolt/config_flow.py create mode 100644 homeassistant/components/indevolt/const.py create mode 100644 homeassistant/components/indevolt/coordinator.py create mode 100644 homeassistant/components/indevolt/entity.py create mode 100644 homeassistant/components/indevolt/manifest.json create mode 100644 homeassistant/components/indevolt/quality_scale.yaml create mode 100644 homeassistant/components/indevolt/sensor.py create mode 100644 homeassistant/components/indevolt/strings.json create mode 100644 tests/components/indevolt/__init__.py create mode 100644 tests/components/indevolt/conftest.py create mode 100644 tests/components/indevolt/fixtures/gen_1.json create mode 100644 tests/components/indevolt/fixtures/gen_2.json create mode 100644 tests/components/indevolt/snapshots/test_sensor.ambr create mode 100644 tests/components/indevolt/test_config_flow.py create mode 100644 tests/components/indevolt/test_init.py create mode 100644 tests/components/indevolt/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index bad799a69e6e1e..bcf8b3d745af18 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -788,6 +788,8 @@ build.json @home-assistant/supervisor /tests/components/improv_ble/ @emontnemery /homeassistant/components/incomfort/ @jbouwh /tests/components/incomfort/ @jbouwh +/homeassistant/components/indevolt/ @xirtnl +/tests/components/indevolt/ @xirtnl /homeassistant/components/inels/ @epdevlab /tests/components/inels/ @epdevlab /homeassistant/components/influxdb/ @mdegat01 diff --git a/homeassistant/components/indevolt/__init__.py b/homeassistant/components/indevolt/__init__.py new file mode 100644 index 00000000000000..a3e045bbf43c46 --- /dev/null +++ b/homeassistant/components/indevolt/__init__.py @@ -0,0 +1,28 @@ +"""Home Assistant integration for indevolt device.""" + +from __future__ import annotations + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .coordinator import IndevoltConfigEntry, IndevoltCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: IndevoltConfigEntry) -> bool: + """Set up indevolt integration entry using given configuration.""" + coordinator = IndevoltCoordinator(hass, entry) + + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: IndevoltConfigEntry) -> bool: + """Unload a config entry / clean up resources (when integration is removed / reloaded).""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/indevolt/config_flow.py b/homeassistant/components/indevolt/config_flow.py new file mode 100644 index 00000000000000..9b813b425da24c --- /dev/null +++ b/homeassistant/components/indevolt/config_flow.py @@ -0,0 +1,83 @@ +"""Config flow for Indevolt integration.""" + +import logging +from typing import Any + +from aiohttp import ClientError +from indevolt_api import IndevoltAPI +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_MODEL +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_GENERATION, CONF_SERIAL_NUMBER, DEFAULT_PORT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class IndevoltConfigFlow(ConfigFlow, domain=DOMAIN): + """Configuration flow for Indevolt integration.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial user configuration step.""" + errors: dict[str, str] = {} + + # Attempt to setup from user input + if user_input is not None: + errors, device_data = await self._async_validate_input(user_input) + + if not errors and device_data: + await self.async_set_unique_id(device_data[CONF_SERIAL_NUMBER]) + + # Handle initial setup + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"INDEVOLT {device_data[CONF_MODEL]}", + data={ + CONF_HOST: user_input[CONF_HOST], + **device_data, + }, + ) + + # Retrieve user input + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors, + ) + + async def _async_validate_input( + self, user_input: dict[str, Any] + ) -> tuple[dict[str, str], dict[str, Any] | None]: + """Validate user input and return errors dict and device data.""" + errors = {} + device_data = None + + try: + device_data = await self._async_get_device_data(user_input[CONF_HOST]) + except TimeoutError: + errors["base"] = "timeout" + except ConnectionError, ClientError: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unknown error occurred while verifying device") + errors["base"] = "unknown" + + return errors, device_data + + async def _async_get_device_data(self, host: str) -> dict[str, Any]: + """Get device data (type, serial number, generation) from API.""" + api = IndevoltAPI(host, DEFAULT_PORT, async_get_clientsession(self.hass)) + config_data = await api.get_config() + device_data = config_data["device"] + + return { + CONF_SERIAL_NUMBER: device_data["sn"], + CONF_GENERATION: device_data["generation"], + CONF_MODEL: device_data["type"], + } diff --git a/homeassistant/components/indevolt/const.py b/homeassistant/components/indevolt/const.py new file mode 100644 index 00000000000000..31dac70f286be4 --- /dev/null +++ b/homeassistant/components/indevolt/const.py @@ -0,0 +1,103 @@ +"""Constants for the Indevolt integration.""" + +DOMAIN = "indevolt" + +# Config entry fields +CONF_SERIAL_NUMBER = "serial_number" +CONF_GENERATION = "generation" + +# Default values +DEFAULT_PORT = 8080 + +# API key fields +SENSOR_KEYS = { + 1: [ + "606", + "7101", + "2101", + "2108", + "2107", + "6000", + "6001", + "6002", + "1501", + "1502", + "1664", + "1665", + "1666", + "1667", + "6105", + "21028", + "1505", + ], + 2: [ + "606", + "7101", + "2101", + "2108", + "2107", + "6000", + "6001", + "6002", + "1501", + "1502", + "1664", + "1665", + "1666", + "1667", + "142", + "667", + "2104", + "2105", + "11034", + "6004", + "6005", + "6006", + "6007", + "11016", + "2600", + "2612", + "1632", + "1600", + "1633", + "1601", + "1634", + "1602", + "1635", + "1603", + "9008", + "9032", + "9051", + "9070", + "9165", + "9218", + "9000", + "9016", + "9035", + "9054", + "9149", + "9202", + "9012", + "9030", + "9049", + "9068", + "9163", + "9216", + "9004", + "9020", + "9039", + "9058", + "9153", + "9206", + "9013", + "19173", + "19174", + "19175", + "19176", + "19177", + "680", + "11011", + "11009", + "11010", + ], +} diff --git a/homeassistant/components/indevolt/coordinator.py b/homeassistant/components/indevolt/coordinator.py new file mode 100644 index 00000000000000..7cf30bfc6f7c15 --- /dev/null +++ b/homeassistant/components/indevolt/coordinator.py @@ -0,0 +1,80 @@ +"""Home Assistant integration for Indevolt device.""" + +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any + +from indevolt_api import IndevoltAPI, TimeOutException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_MODEL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_GENERATION, + CONF_SERIAL_NUMBER, + DEFAULT_PORT, + DOMAIN, + SENSOR_KEYS, +) + +_LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = 30 + +type IndevoltConfigEntry = ConfigEntry[IndevoltCoordinator] + + +class IndevoltCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Coordinator for fetching and pushing data to indevolt devices.""" + + config_entry: IndevoltConfigEntry + firmware_version: str | None + + def __init__(self, hass: HomeAssistant, entry: IndevoltConfigEntry) -> None: + """Initialize the indevolt coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=SCAN_INTERVAL), + config_entry=entry, + ) + + # Initialize Indevolt API + self.api = IndevoltAPI( + host=entry.data[CONF_HOST], + port=DEFAULT_PORT, + session=async_get_clientsession(hass), + ) + + self.serial_number = entry.data[CONF_SERIAL_NUMBER] + self.device_model = entry.data[CONF_MODEL] + self.generation = entry.data[CONF_GENERATION] + + async def _async_setup(self) -> None: + """Fetch device info once on boot.""" + try: + config_data = await self.api.get_config() + except TimeOutException as err: + raise ConfigEntryNotReady( + f"Device config retrieval timed out: {err}" + ) from err + + # Cache device information + device_data = config_data.get("device", {}) + + self.firmware_version = device_data.get("fw") + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch raw JSON data from the device.""" + sensor_keys = SENSOR_KEYS[self.generation] + + try: + return await self.api.fetch_data(sensor_keys) + except TimeOutException as err: + raise UpdateFailed(f"Device update timed out: {err}") from err diff --git a/homeassistant/components/indevolt/entity.py b/homeassistant/components/indevolt/entity.py new file mode 100644 index 00000000000000..da87036a33a3da --- /dev/null +++ b/homeassistant/components/indevolt/entity.py @@ -0,0 +1,31 @@ +"""Base entity for Indevolt integration.""" + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import IndevoltCoordinator + + +class IndevoltEntity(CoordinatorEntity[IndevoltCoordinator]): + """Base Indevolt entity with up-to-date device info.""" + + _attr_has_entity_name = True + + @property + def serial_number(self) -> str: + """Return the device serial number.""" + return self.coordinator.serial_number + + @property + def device_info(self) -> DeviceInfo: + """Return device information for registry.""" + coordinator = self.coordinator + return DeviceInfo( + identifiers={(DOMAIN, coordinator.serial_number)}, + manufacturer="INDEVOLT", + serial_number=coordinator.serial_number, + model=coordinator.device_model, + sw_version=coordinator.firmware_version, + hw_version=str(coordinator.generation), + ) diff --git a/homeassistant/components/indevolt/manifest.json b/homeassistant/components/indevolt/manifest.json new file mode 100644 index 00000000000000..f85e9745b7560d --- /dev/null +++ b/homeassistant/components/indevolt/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "indevolt", + "name": "Indevolt", + "codeowners": ["@xirtnl"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/indevolt", + "integration_type": "device", + "iot_class": "local_polling", + "quality_scale": "bronze", + "requirements": ["indevolt-api==1.1.2"] +} diff --git a/homeassistant/components/indevolt/quality_scale.yaml b/homeassistant/components/indevolt/quality_scale.yaml new file mode 100644 index 00000000000000..c436beb43fe680 --- /dev/null +++ b/homeassistant/components/indevolt/quality_scale.yaml @@ -0,0 +1,94 @@ +rules: + # Bronze (mandatory for core integrations) + action-setup: + status: exempt + comment: Integration does not register custom actions + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: Integration does not register custom actions + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: Integration does not subscribe to entity events, uses coordinator pattern + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: + status: exempt + comment: Integration does not register custom actions + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: Integration has no user-configurable parameters beyond setup + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: done + reauthentication-flow: + status: exempt + comment: Integration uses local device with no authentication + test-coverage: done + + # Gold + devices: done + diagnostics: + status: todo + discovery-update-info: + status: exempt + comment: Integration does not support network discovery + discovery: + status: exempt + comment: Integration does not support network discovery + docs-data-update: + status: todo + docs-examples: + status: todo + docs-known-limitations: + status: todo + docs-supported-devices: + status: todo + docs-supported-functions: + status: todo + docs-troubleshooting: + status: todo + docs-use-cases: + status: todo + dynamic-devices: + status: exempt + comment: Integration represents a single device, not a hub with multiple devices + entity-category: done + entity-device-class: done + entity-disabled-by-default: done + entity-translations: done + exception-translations: + status: todo + icon-translations: + status: todo + reconfiguration-flow: + status: todo + repair-issues: + status: exempt + comment: No repair issues needed for current functionality + stale-devices: + status: exempt + comment: Integration represents a single device, not a hub with multiple devices + + # Platinum + async-dependency: done + inject-websession: done + strict-typing: + status: todo diff --git a/homeassistant/components/indevolt/sensor.py b/homeassistant/components/indevolt/sensor.py new file mode 100644 index 00000000000000..3ccfeec8571cdb --- /dev/null +++ b/homeassistant/components/indevolt/sensor.py @@ -0,0 +1,704 @@ +"""Sensor platform for Indevolt integration.""" + +from dataclasses import dataclass, field +from typing import Final + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + EntityCategory, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfPower, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import IndevoltConfigEntry +from .coordinator import IndevoltCoordinator +from .entity import IndevoltEntity + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class IndevoltSensorEntityDescription(SensorEntityDescription): + """Custom entity description class for Indevolt sensors.""" + + state_mapping: dict[str | int, str] = field(default_factory=dict) + generation: list[int] = field(default_factory=lambda: [1, 2]) + + +SENSORS: Final = ( + # System Operating Information + IndevoltSensorEntityDescription( + key="606", + translation_key="mode", + state_mapping={"1000": "main", "1001": "sub", "1002": "standalone"}, + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="7101", + translation_key="energy_mode", + state_mapping={ + 0: "outdoor_portable", + 1: "self_consumed_prioritized", + 4: "real_time_control", + 5: "charge_discharge_schedule", + }, + device_class=SensorDeviceClass.ENUM, + ), + IndevoltSensorEntityDescription( + key="142", + generation=[2], + translation_key="rated_capacity", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IndevoltSensorEntityDescription( + key="6105", + generation=[1], + translation_key="rated_capacity", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IndevoltSensorEntityDescription( + key="2101", + translation_key="ac_input_power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + IndevoltSensorEntityDescription( + key="2108", + translation_key="ac_output_power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + IndevoltSensorEntityDescription( + key="667", + generation=[2], + translation_key="bypass_power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + # Electrical Energy Information + IndevoltSensorEntityDescription( + key="2107", + translation_key="total_ac_input_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IndevoltSensorEntityDescription( + key="2104", + generation=[2], + translation_key="total_ac_output_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IndevoltSensorEntityDescription( + key="2105", + generation=[2], + translation_key="off_grid_output_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IndevoltSensorEntityDescription( + key="11034", + generation=[2], + translation_key="bypass_input_energy", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IndevoltSensorEntityDescription( + key="6004", + generation=[2], + translation_key="battery_daily_charging_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IndevoltSensorEntityDescription( + key="6005", + generation=[2], + translation_key="battery_daily_discharging_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IndevoltSensorEntityDescription( + key="6006", + generation=[2], + translation_key="battery_total_charging_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IndevoltSensorEntityDescription( + key="6007", + generation=[2], + translation_key="battery_total_discharging_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + # Electricity Meter Status + IndevoltSensorEntityDescription( + key="11016", + generation=[2], + translation_key="meter_power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + IndevoltSensorEntityDescription( + key="21028", + generation=[1], + translation_key="meter_power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + # Grid information + IndevoltSensorEntityDescription( + key="2600", + generation=[2], + translation_key="grid_voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="2612", + generation=[2], + translation_key="grid_frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + # Battery Pack Operating Parameters + IndevoltSensorEntityDescription( + key="6000", + translation_key="battery_power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + IndevoltSensorEntityDescription( + key="6001", + translation_key="battery_charge_discharge_state", + state_mapping={1000: "static", 1001: "charging", 1002: "discharging"}, + device_class=SensorDeviceClass.ENUM, + ), + IndevoltSensorEntityDescription( + key="6002", + translation_key="battery_soc", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + ), + # PV Operating Parameters + IndevoltSensorEntityDescription( + key="1501", + translation_key="dc_output_power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + IndevoltSensorEntityDescription( + key="1502", + translation_key="daily_production", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IndevoltSensorEntityDescription( + key="1505", + generation=[1], + translation_key="cumulative_production", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IndevoltSensorEntityDescription( + key="1632", + generation=[2], + translation_key="dc_input_current_1", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="1600", + generation=[2], + translation_key="dc_input_voltage_1", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="1664", + translation_key="dc_input_power_1", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="1633", + generation=[2], + translation_key="dc_input_current_2", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="1601", + generation=[2], + translation_key="dc_input_voltage_2", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="1665", + translation_key="dc_input_power_2", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="1634", + generation=[2], + translation_key="dc_input_current_3", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="1602", + generation=[2], + translation_key="dc_input_voltage_3", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="1666", + generation=[2], + translation_key="dc_input_power_3", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="1635", + generation=[2], + translation_key="dc_input_current_4", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="1603", + generation=[2], + translation_key="dc_input_voltage_4", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="1667", + generation=[2], + translation_key="dc_input_power_4", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + # Battery Pack Serial Numbers + IndevoltSensorEntityDescription( + key="9008", + generation=[2], + translation_key="main_serial_number", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9032", + generation=[2], + translation_key="battery_pack_1_serial_number", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9051", + generation=[2], + translation_key="battery_pack_2_serial_number", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9070", + generation=[2], + translation_key="battery_pack_3_serial_number", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9165", + generation=[2], + translation_key="battery_pack_4_serial_number", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9218", + generation=[2], + translation_key="battery_pack_5_serial_number", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + # Battery Pack SOC + IndevoltSensorEntityDescription( + key="9000", + generation=[2], + translation_key="main_soc", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9016", + generation=[2], + translation_key="battery_pack_1_soc", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9035", + generation=[2], + translation_key="battery_pack_2_soc", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9054", + generation=[2], + translation_key="battery_pack_3_soc", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9149", + generation=[2], + translation_key="battery_pack_4_soc", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9202", + generation=[2], + translation_key="battery_pack_5_soc", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + # Battery Pack Temperature + IndevoltSensorEntityDescription( + key="9012", + generation=[2], + translation_key="main_temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9030", + generation=[2], + translation_key="battery_pack_1_temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9049", + generation=[2], + translation_key="battery_pack_2_temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9068", + generation=[2], + translation_key="battery_pack_3_temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9163", + generation=[2], + translation_key="battery_pack_4_temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9216", + generation=[2], + translation_key="battery_pack_5_temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + # Battery Pack Voltage + IndevoltSensorEntityDescription( + key="9004", + generation=[2], + translation_key="main_voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9020", + generation=[2], + translation_key="battery_pack_1_voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9039", + generation=[2], + translation_key="battery_pack_2_voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9058", + generation=[2], + translation_key="battery_pack_3_voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9153", + generation=[2], + translation_key="battery_pack_4_voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="9206", + generation=[2], + translation_key="battery_pack_5_voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + # Battery Pack Current + IndevoltSensorEntityDescription( + key="9013", + generation=[2], + translation_key="main_current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="19173", + generation=[2], + translation_key="battery_pack_1_current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="19174", + generation=[2], + translation_key="battery_pack_2_current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="19175", + generation=[2], + translation_key="battery_pack_3_current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="19176", + generation=[2], + translation_key="battery_pack_4_current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltSensorEntityDescription( + key="19177", + generation=[2], + translation_key="battery_pack_5_current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), +) + +# Sensors per battery pack (SN, SOC, Temperature, Voltage, Current) +BATTERY_PACK_SENSOR_KEYS = [ + ("9032", "9016", "9030", "9020", "19173"), # Battery Pack 1 + ("9051", "9035", "9049", "9039", "19174"), # Battery Pack 2 + ("9070", "9054", "9068", "9058", "19175"), # Battery Pack 3 + ("9165", "9149", "9163", "9153", "19176"), # Battery Pack 4 + ("9218", "9202", "9216", "9206", "19177"), # Battery Pack 5 +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: IndevoltConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the sensor platform for Indevolt.""" + coordinator = entry.runtime_data + device_gen = coordinator.generation + + excluded_keys: set[str] = set() + for pack_keys in BATTERY_PACK_SENSOR_KEYS: + sn_key = pack_keys[0] + + if not coordinator.data.get(sn_key): + excluded_keys.update(pack_keys) + + # Sensor initialization + async_add_entities( + IndevoltSensorEntity(coordinator, description) + for description in SENSORS + if device_gen in description.generation and description.key not in excluded_keys + ) + + +class IndevoltSensorEntity(IndevoltEntity, SensorEntity): + """Represents a sensor entity for Indevolt devices.""" + + entity_description: IndevoltSensorEntityDescription + + def __init__( + self, + coordinator: IndevoltCoordinator, + description: IndevoltSensorEntityDescription, + ) -> None: + """Initialize the Indevolt sensor entity.""" + super().__init__(coordinator) + + self.entity_description = description + self._attr_unique_id = f"{self.serial_number}_{description.key}" + + # Sort options (prevent randomization) for ENUM values + if description.device_class == SensorDeviceClass.ENUM: + self._attr_options = sorted(set(description.state_mapping.values())) + + @property + def native_value(self) -> str | int | float | None: + """Return the current value of the sensor in its native unit.""" + raw_value = self.coordinator.data.get(self.entity_description.key) + if raw_value is None: + return None + + # Return descriptions for ENUM values + if self.entity_description.device_class == SensorDeviceClass.ENUM: + return self.entity_description.state_mapping.get(raw_value) + + return raw_value diff --git a/homeassistant/components/indevolt/strings.json b/homeassistant/components/indevolt/strings.json new file mode 100644 index 00000000000000..848378a86c4308 --- /dev/null +++ b/homeassistant/components/indevolt/strings.json @@ -0,0 +1,246 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "Failed to connect (aborted)" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout": "[%key:common::config_flow::error::timeout_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The host of the Indevolt device" + }, + "description": "Enter the connection details for your Indevolt device.", + "title": "Connect to Indevolt device" + } + } + }, + "entity": { + "sensor": { + "ac_input_power": { + "name": "AC input power" + }, + "ac_output_power": { + "name": "AC output power" + }, + "battery_charge_discharge_state": { + "name": "Battery charge/discharge state", + "state": { + "charging": "Charging", + "discharging": "Discharging", + "static": "Static" + } + }, + "battery_daily_charging_energy": { + "name": "Battery daily charging energy" + }, + "battery_daily_discharging_energy": { + "name": "Battery daily discharging energy" + }, + "battery_pack_1_current": { + "name": "Battery pack 1 current" + }, + "battery_pack_1_serial_number": { + "name": "Battery pack 1 SN" + }, + "battery_pack_1_soc": { + "name": "Battery pack 1 SOC" + }, + "battery_pack_1_temperature": { + "name": "Battery pack 1 temperature" + }, + "battery_pack_1_voltage": { + "name": "Battery pack 1 voltage" + }, + "battery_pack_2_current": { + "name": "Battery pack 2 current" + }, + "battery_pack_2_serial_number": { + "name": "Battery pack 2 SN" + }, + "battery_pack_2_soc": { + "name": "Battery pack 2 SOC" + }, + "battery_pack_2_temperature": { + "name": "Battery pack 2 temperature" + }, + "battery_pack_2_voltage": { + "name": "Battery pack 2 voltage" + }, + "battery_pack_3_current": { + "name": "Battery pack 3 current" + }, + "battery_pack_3_serial_number": { + "name": "Battery pack 3 SN" + }, + "battery_pack_3_soc": { + "name": "Battery pack 3 SOC" + }, + "battery_pack_3_temperature": { + "name": "Battery pack 3 temperature" + }, + "battery_pack_3_voltage": { + "name": "Battery pack 3 voltage" + }, + "battery_pack_4_current": { + "name": "Battery pack 4 current" + }, + "battery_pack_4_serial_number": { + "name": "Battery pack 4 SN" + }, + "battery_pack_4_soc": { + "name": "Battery pack 4 SOC" + }, + "battery_pack_4_temperature": { + "name": "Battery pack 4 temperature" + }, + "battery_pack_4_voltage": { + "name": "Battery pack 4 voltage" + }, + "battery_pack_5_current": { + "name": "Battery pack 5 current" + }, + "battery_pack_5_serial_number": { + "name": "Battery pack 5 SN" + }, + "battery_pack_5_soc": { + "name": "Battery pack 5 SOC" + }, + "battery_pack_5_temperature": { + "name": "Battery pack 5 temperature" + }, + "battery_pack_5_voltage": { + "name": "Battery pack 5 voltage" + }, + "battery_power": { + "name": "Battery power" + }, + "battery_soc": { + "name": "Battery SOC" + }, + "battery_total_charging_energy": { + "name": "Battery total charging energy" + }, + "battery_total_discharging_energy": { + "name": "Battery total discharging energy" + }, + "bypass_input_energy": { + "name": "Bypass input energy" + }, + "bypass_power": { + "name": "Bypass power" + }, + "cumulative_production": { + "name": "Cumulative production" + }, + "daily_production": { + "name": "Daily production" + }, + "dc_input_current_1": { + "name": "DC input current 1" + }, + "dc_input_current_2": { + "name": "DC input current 2" + }, + "dc_input_current_3": { + "name": "DC input current 3" + }, + "dc_input_current_4": { + "name": "DC input current 4" + }, + "dc_input_power_1": { + "name": "DC input power 1" + }, + "dc_input_power_2": { + "name": "DC input power 2" + }, + "dc_input_power_3": { + "name": "DC input power 3" + }, + "dc_input_power_4": { + "name": "DC input power 4" + }, + "dc_input_voltage_1": { + "name": "DC input voltage 1" + }, + "dc_input_voltage_2": { + "name": "DC input voltage 2" + }, + "dc_input_voltage_3": { + "name": "DC input voltage 3" + }, + "dc_input_voltage_4": { + "name": "DC input voltage 4" + }, + "dc_output_power": { + "name": "DC output power" + }, + "energy_mode": { + "name": "Energy mode", + "state": { + "charge_discharge_schedule": "Charge/discharge schedule", + "outdoor_portable": "Outdoor portable", + "real_time_control": "Real-time control", + "self_consumed_prioritized": "Self-consumed prioritized" + } + }, + "grid_frequency": { + "name": "Grid frequency" + }, + "grid_voltage": { + "name": "Grid voltage" + }, + "main_current": { + "name": "Main current" + }, + "main_serial_number": { + "name": "Main serial number" + }, + "main_soc": { + "name": "Main SOC" + }, + "main_temperature": { + "name": "Main temperature" + }, + "main_voltage": { + "name": "Main voltage" + }, + "meter_power": { + "name": "Meter power" + }, + "mode": { + "name": "Device mode", + "state": { + "main": "Cluster (main)", + "standalone": "Standalone", + "sub": "Cluster (sub)" + } + }, + "off_grid_output_energy": { + "name": "Off-grid output energy" + }, + "rated_capacity": { + "name": "Rated capacity" + }, + "serial_number": { + "name": "Serial number" + }, + "total_ac_input_energy": { + "name": "Total AC input energy" + }, + "total_ac_input_energy_gen1": { + "name": "Total AC input energy" + }, + "total_ac_output_energy": { + "name": "Total AC output energy" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9bcc030329a647..156029ea0f0636 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -329,6 +329,7 @@ "immich", "improv_ble", "incomfort", + "indevolt", "inels", "inkbird", "insteon", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e7935ab74279d6..38063333132953 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3116,6 +3116,12 @@ "config_flow": true, "iot_class": "local_polling" }, + "indevolt": { + "name": "Indevolt", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling" + }, "indianamichiganpower": { "name": "Indiana Michigan Power", "integration_type": "virtual", diff --git a/requirements_all.txt b/requirements_all.txt index 33939a98d47ad6..0a75b3319fd3cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1306,6 +1306,9 @@ imgw_pib==2.0.1 # homeassistant.components.incomfort incomfort-client==0.6.12 +# homeassistant.components.indevolt +indevolt-api==1.1.2 + # homeassistant.components.influxdb influxdb-client==1.50.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 183cd008f2f61c..886cb1e7413a86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1155,6 +1155,9 @@ imgw_pib==2.0.1 # homeassistant.components.incomfort incomfort-client==0.6.12 +# homeassistant.components.indevolt +indevolt-api==1.1.2 + # homeassistant.components.influxdb influxdb-client==1.50.0 diff --git a/tests/components/indevolt/__init__.py b/tests/components/indevolt/__init__.py new file mode 100644 index 00000000000000..87ece922797dbc --- /dev/null +++ b/tests/components/indevolt/__init__.py @@ -0,0 +1,14 @@ +"""Tests for the Indevolt integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Set up the integration for testing.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/indevolt/conftest.py b/tests/components/indevolt/conftest.py new file mode 100644 index 00000000000000..a79fdc2d3fbd58 --- /dev/null +++ b/tests/components/indevolt/conftest.py @@ -0,0 +1,108 @@ +"""Setup the Indevolt test environment.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.indevolt.const import ( + CONF_GENERATION, + CONF_SERIAL_NUMBER, + DOMAIN, +) +from homeassistant.const import CONF_HOST, CONF_MODEL + +from tests.common import MockConfigEntry, load_json_object_fixture + +TEST_HOST = "192.168.1.100" +TEST_PORT = 8080 +TEST_DEVICE_SN_GEN1 = "BK1600-12345678" +TEST_DEVICE_SN_GEN2 = "SolidFlex2000-87654321" +TEST_FW_VERSION = "1.2.3" + +# Map device fixture names to generation and fixture files +DEVICE_MAPPING = { + 1: { + "device": "BK1600", + "generation": 1, + "sn": TEST_DEVICE_SN_GEN1, + }, + 2: { + "device": "CMS-SF2000", + "generation": 2, + "sn": TEST_DEVICE_SN_GEN2, + }, +} + + +@pytest.fixture +def generation(request: pytest.FixtureRequest) -> int: + """Return the device generation.""" + return getattr(request, "param", 2) + + +@pytest.fixture +def entry_data(generation: int) -> dict[str, Any]: + """Return the config entry data based on generation.""" + device_info = DEVICE_MAPPING[generation] + return { + CONF_HOST: TEST_HOST, + CONF_SERIAL_NUMBER: device_info["sn"], + CONF_MODEL: device_info["device"], + CONF_GENERATION: device_info["generation"], + } + + +@pytest.fixture +def mock_config_entry(generation: int, entry_data: dict[str, Any]) -> MockConfigEntry: + """Return the default mocked config entry.""" + device_info = DEVICE_MAPPING[generation] + return MockConfigEntry( + domain=DOMAIN, + title=device_info["device"], + version=1, + data=entry_data, + unique_id=device_info["sn"], + ) + + +@pytest.fixture +def mock_indevolt(generation: int) -> Generator[AsyncMock]: + """Mock an IndevoltAPI client.""" + device_info = DEVICE_MAPPING[generation] + fixture_data = load_json_object_fixture(f"gen_{generation}.json", DOMAIN) + + with ( + patch( + "homeassistant.components.indevolt.coordinator.IndevoltAPI", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.indevolt.config_flow.IndevoltAPI", + new=mock_client, + ), + ): + # Mock coordinator API (get_data) + client = mock_client.return_value + client.fetch_data.return_value = fixture_data + client.get_config.return_value = { + "device": { + "sn": device_info["sn"], + "type": device_info["device"], + "generation": device_info["generation"], + "fw": TEST_FW_VERSION, + } + } + + yield client + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Mock the async_setup_entry function.""" + with patch( + "homeassistant.components.indevolt.async_setup_entry", + return_value=True, + ) as mock_setup: + yield mock_setup diff --git a/tests/components/indevolt/fixtures/gen_1.json b/tests/components/indevolt/fixtures/gen_1.json new file mode 100644 index 00000000000000..46269f8396f250 --- /dev/null +++ b/tests/components/indevolt/fixtures/gen_1.json @@ -0,0 +1,23 @@ +{ + "0": "BK1600-12345678", + "606": "1000", + "7101": 5, + "1664": 0, + "1665": 0, + "2108": 0, + "1502": 0, + "1505": 553673, + "2101": 0, + "2107": 58.1, + "1501": 0, + "6000": 0, + "6001": 1000, + "6002": 92, + "6105": 5, + "6004": 0, + "6005": 0, + "6006": 277.16, + "6007": 256.39, + "7120": 1001, + "21028": 0 +} diff --git a/tests/components/indevolt/fixtures/gen_2.json b/tests/components/indevolt/fixtures/gen_2.json new file mode 100644 index 00000000000000..7643daedd249f4 --- /dev/null +++ b/tests/components/indevolt/fixtures/gen_2.json @@ -0,0 +1,74 @@ +{ + "0": "SolidFlex2000-87654321", + "606": "1001", + "7101": 1, + "142": 1.79, + "6105": 5, + "2618": 250.5, + "11009": 50.2, + "2101": 0, + "2108": 0, + "11010": 52.3, + "667": 0, + "2107": 289.97, + "2104": 1500, + "2105": 2000, + "11034": 100, + "1502": 0, + "6004": 0.07, + "6005": 0, + "6006": 380.58, + "6007": 338.07, + "7120": 1001, + "11016": 0, + "2600": 1200, + "2612": 50.0, + "6001": 1000, + "6000": 0, + "6002": 92, + "1501": 0, + "1532": 150, + "1600": 48.5, + "1632": 10.2, + "1664": 0, + "1633": 10.1, + "1601": 48.3, + "1665": 0, + "1634": 9.8, + "1602": 48.7, + "1666": 0, + "1635": 9.9, + "1603": 48.6, + "1667": 0, + "11011": 85, + "9008": "MASTER-12345678", + "9032": "PACK1-11111111", + "9051": "PACK2-22222222", + "9070": "PACK3-33333333", + "9165": "PACK4-44444444", + "9218": "PACK5-55555555", + "9000": 92, + "9016": 91, + "9035": 93, + "9054": 92, + "9149": 94, + "9202": 90, + "9012": 25.5, + "9030": 24.8, + "9049": 25.2, + "9068": 25.0, + "9163": 25.7, + "9216": 24.9, + "9004": 51.2, + "9020": 51.0, + "9039": 51.3, + "9058": 51.1, + "9153": 51.4, + "9206": 50.9, + "9013": 15.2, + "19173": 14.8, + "19174": 15.0, + "19175": 15.1, + "19176": 15.3, + "19177": 14.9 +} diff --git a/tests/components/indevolt/snapshots/test_sensor.ambr b/tests/components/indevolt/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..48e3194e78178f --- /dev/null +++ b/tests/components/indevolt/snapshots/test_sensor.ambr @@ -0,0 +1,4463 @@ +# serializer version: 1 +# name: test_sensor[1][sensor.bk1600_ac_input_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_ac_input_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'AC input power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC input power', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ac_input_power', + 'unique_id': 'BK1600-12345678_2101', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[1][sensor.bk1600_ac_input_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'BK1600 AC input power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bk1600_ac_input_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[1][sensor.bk1600_ac_output_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_ac_output_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'AC output power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC output power', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ac_output_power', + 'unique_id': 'BK1600-12345678_2108', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[1][sensor.bk1600_ac_output_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'BK1600 AC output power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bk1600_ac_output_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[1][sensor.bk1600_battery_charge_discharge_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'charging', + 'discharging', + 'static', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_battery_charge_discharge_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery charge/discharge state', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery charge/discharge state', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_charge_discharge_state', + 'unique_id': 'BK1600-12345678_6001', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[1][sensor.bk1600_battery_charge_discharge_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'BK1600 Battery charge/discharge state', + 'options': list([ + 'charging', + 'discharging', + 'static', + ]), + }), + 'context': , + 'entity_id': 'sensor.bk1600_battery_charge_discharge_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'static', + }) +# --- +# name: test_sensor[1][sensor.bk1600_battery_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_battery_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery power', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_power', + 'unique_id': 'BK1600-12345678_6000', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[1][sensor.bk1600_battery_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'BK1600 Battery power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bk1600_battery_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[1][sensor.bk1600_battery_soc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_battery_soc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery SOC', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery SOC', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_soc', + 'unique_id': 'BK1600-12345678_6002', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[1][sensor.bk1600_battery_soc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'BK1600 Battery SOC', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.bk1600_battery_soc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensor[1][sensor.bk1600_cumulative_production-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_cumulative_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Cumulative production', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cumulative production', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cumulative_production', + 'unique_id': 'BK1600-12345678_1505', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[1][sensor.bk1600_cumulative_production-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'BK1600 Cumulative production', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bk1600_cumulative_production', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '553.673', + }) +# --- +# name: test_sensor[1][sensor.bk1600_daily_production-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_daily_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Daily production', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daily production', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production', + 'unique_id': 'BK1600-12345678_1502', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[1][sensor.bk1600_daily_production-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'BK1600 Daily production', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bk1600_daily_production', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[1][sensor.bk1600_dc_input_power_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_dc_input_power_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input power 1', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input power 1', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_power_1', + 'unique_id': 'BK1600-12345678_1664', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[1][sensor.bk1600_dc_input_power_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'BK1600 DC input power 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bk1600_dc_input_power_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[1][sensor.bk1600_dc_input_power_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_dc_input_power_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input power 2', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input power 2', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_power_2', + 'unique_id': 'BK1600-12345678_1665', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[1][sensor.bk1600_dc_input_power_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'BK1600 DC input power 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bk1600_dc_input_power_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[1][sensor.bk1600_dc_output_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_dc_output_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC output power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC output power', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_output_power', + 'unique_id': 'BK1600-12345678_1501', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[1][sensor.bk1600_dc_output_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'BK1600 DC output power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bk1600_dc_output_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[1][sensor.bk1600_device_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'main', + 'standalone', + 'sub', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.bk1600_device_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Device mode', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Device mode', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mode', + 'unique_id': 'BK1600-12345678_606', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[1][sensor.bk1600_device_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'BK1600 Device mode', + 'options': list([ + 'main', + 'standalone', + 'sub', + ]), + }), + 'context': , + 'entity_id': 'sensor.bk1600_device_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'main', + }) +# --- +# name: test_sensor[1][sensor.bk1600_energy_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'charge_discharge_schedule', + 'outdoor_portable', + 'real_time_control', + 'self_consumed_prioritized', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_energy_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Energy mode', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy mode', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_mode', + 'unique_id': 'BK1600-12345678_7101', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[1][sensor.bk1600_energy_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'BK1600 Energy mode', + 'options': list([ + 'charge_discharge_schedule', + 'outdoor_portable', + 'real_time_control', + 'self_consumed_prioritized', + ]), + }), + 'context': , + 'entity_id': 'sensor.bk1600_energy_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charge_discharge_schedule', + }) +# --- +# name: test_sensor[1][sensor.bk1600_meter_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_meter_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Meter power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Meter power', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'meter_power', + 'unique_id': 'BK1600-12345678_21028', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[1][sensor.bk1600_meter_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'BK1600 Meter power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bk1600_meter_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[1][sensor.bk1600_rated_capacity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_rated_capacity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Rated capacity', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Rated capacity', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'rated_capacity', + 'unique_id': 'BK1600-12345678_6105', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[1][sensor.bk1600_rated_capacity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'BK1600 Rated capacity', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bk1600_rated_capacity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_sensor[1][sensor.bk1600_total_ac_input_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bk1600_total_ac_input_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total AC input energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total AC input energy', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_ac_input_energy', + 'unique_id': 'BK1600-12345678_2107', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[1][sensor.bk1600_total_ac_input_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'BK1600 Total AC input energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bk1600_total_ac_input_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '58.1', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_ac_input_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_ac_input_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'AC input power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC input power', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ac_input_power', + 'unique_id': 'SolidFlex2000-87654321_2101', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_ac_input_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'CMS-SF2000 AC input power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_ac_input_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_ac_output_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_ac_output_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'AC output power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC output power', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ac_output_power', + 'unique_id': 'SolidFlex2000-87654321_2108', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_ac_output_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'CMS-SF2000 AC output power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_ac_output_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_charge_discharge_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'charging', + 'discharging', + 'static', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_battery_charge_discharge_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery charge/discharge state', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery charge/discharge state', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_charge_discharge_state', + 'unique_id': 'SolidFlex2000-87654321_6001', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_charge_discharge_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'CMS-SF2000 Battery charge/discharge state', + 'options': list([ + 'charging', + 'discharging', + 'static', + ]), + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_charge_discharge_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'static', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_daily_charging_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_battery_daily_charging_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery daily charging energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery daily charging energy', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_daily_charging_energy', + 'unique_id': 'SolidFlex2000-87654321_6004', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_daily_charging_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'CMS-SF2000 Battery daily charging energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_daily_charging_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.07', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_daily_discharging_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_battery_daily_discharging_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery daily discharging energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery daily discharging energy', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_daily_discharging_energy', + 'unique_id': 'SolidFlex2000-87654321_6005', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_daily_discharging_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'CMS-SF2000 Battery daily discharging energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_daily_discharging_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_1_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_1_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 1 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 1 current', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_1_current', + 'unique_id': 'SolidFlex2000-87654321_19173', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_1_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'CMS-SF2000 Battery pack 1 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.8', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_1_sn-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_1_sn', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 1 SN', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery pack 1 SN', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_1_serial_number', + 'unique_id': 'SolidFlex2000-87654321_9032', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_1_sn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Battery pack 1 SN', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_1_sn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'PACK1-11111111', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_1_soc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_1_soc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 1 SOC', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 1 SOC', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_1_soc', + 'unique_id': 'SolidFlex2000-87654321_9016', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_1_soc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'CMS-SF2000 Battery pack 1 SOC', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_1_soc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '91', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_1_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_1_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 1 temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 1 temperature', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_1_temperature', + 'unique_id': 'SolidFlex2000-87654321_9030', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_1_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'CMS-SF2000 Battery pack 1 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_1_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.8', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_1_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_1_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 1 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 1 voltage', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_1_voltage', + 'unique_id': 'SolidFlex2000-87654321_9020', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_1_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'CMS-SF2000 Battery pack 1 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '51.0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_2_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_2_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 2 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 2 current', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_2_current', + 'unique_id': 'SolidFlex2000-87654321_19174', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_2_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'CMS-SF2000 Battery pack 2 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_2_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_2_sn-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_2_sn', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 2 SN', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery pack 2 SN', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_2_serial_number', + 'unique_id': 'SolidFlex2000-87654321_9051', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_2_sn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Battery pack 2 SN', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_2_sn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'PACK2-22222222', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_2_soc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_2_soc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 2 SOC', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 2 SOC', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_2_soc', + 'unique_id': 'SolidFlex2000-87654321_9035', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_2_soc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'CMS-SF2000 Battery pack 2 SOC', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_2_soc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '93', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_2_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_2_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 2 temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 2 temperature', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_2_temperature', + 'unique_id': 'SolidFlex2000-87654321_9049', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_2_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'CMS-SF2000 Battery pack 2 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_2_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.2', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_2_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_2_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 2 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 2 voltage', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_2_voltage', + 'unique_id': 'SolidFlex2000-87654321_9039', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_2_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'CMS-SF2000 Battery pack 2 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_2_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '51.3', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_3_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_3_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 3 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 3 current', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_3_current', + 'unique_id': 'SolidFlex2000-87654321_19175', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_3_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'CMS-SF2000 Battery pack 3 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_3_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.1', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_3_sn-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_3_sn', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 3 SN', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery pack 3 SN', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_3_serial_number', + 'unique_id': 'SolidFlex2000-87654321_9070', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_3_sn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Battery pack 3 SN', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_3_sn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'PACK3-33333333', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_3_soc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_3_soc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 3 SOC', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 3 SOC', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_3_soc', + 'unique_id': 'SolidFlex2000-87654321_9054', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_3_soc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'CMS-SF2000 Battery pack 3 SOC', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_3_soc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_3_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_3_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 3 temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 3 temperature', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_3_temperature', + 'unique_id': 'SolidFlex2000-87654321_9068', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_3_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'CMS-SF2000 Battery pack 3 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_3_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_3_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_3_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 3 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 3 voltage', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_3_voltage', + 'unique_id': 'SolidFlex2000-87654321_9058', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_3_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'CMS-SF2000 Battery pack 3 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_3_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '51.1', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_4_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_4_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 4 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 4 current', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_4_current', + 'unique_id': 'SolidFlex2000-87654321_19176', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_4_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'CMS-SF2000 Battery pack 4 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_4_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.3', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_4_sn-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_4_sn', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 4 SN', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery pack 4 SN', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_4_serial_number', + 'unique_id': 'SolidFlex2000-87654321_9165', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_4_sn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Battery pack 4 SN', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_4_sn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'PACK4-44444444', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_4_soc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_4_soc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 4 SOC', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 4 SOC', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_4_soc', + 'unique_id': 'SolidFlex2000-87654321_9149', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_4_soc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'CMS-SF2000 Battery pack 4 SOC', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_4_soc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '94', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_4_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_4_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 4 temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 4 temperature', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_4_temperature', + 'unique_id': 'SolidFlex2000-87654321_9163', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_4_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'CMS-SF2000 Battery pack 4 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_4_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.7', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_4_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_4_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 4 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 4 voltage', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_4_voltage', + 'unique_id': 'SolidFlex2000-87654321_9153', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_4_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'CMS-SF2000 Battery pack 4 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_4_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '51.4', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_5_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_5_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 5 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 5 current', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_5_current', + 'unique_id': 'SolidFlex2000-87654321_19177', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_5_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'CMS-SF2000 Battery pack 5 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_5_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.9', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_5_sn-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_5_sn', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 5 SN', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery pack 5 SN', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_5_serial_number', + 'unique_id': 'SolidFlex2000-87654321_9218', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_5_sn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Battery pack 5 SN', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_5_sn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'PACK5-55555555', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_5_soc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_5_soc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 5 SOC', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 5 SOC', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_5_soc', + 'unique_id': 'SolidFlex2000-87654321_9202', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_5_soc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'CMS-SF2000 Battery pack 5 SOC', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_5_soc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '90', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_5_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_5_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 5 temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 5 temperature', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_5_temperature', + 'unique_id': 'SolidFlex2000-87654321_9216', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_5_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'CMS-SF2000 Battery pack 5 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_5_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.9', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_5_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_5_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 5 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery pack 5 voltage', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_5_voltage', + 'unique_id': 'SolidFlex2000-87654321_9206', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_pack_5_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'CMS-SF2000 Battery pack 5 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_pack_5_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50.9', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_battery_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery power', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_power', + 'unique_id': 'SolidFlex2000-87654321_6000', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'CMS-SF2000 Battery power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_soc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_battery_soc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery SOC', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery SOC', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_soc', + 'unique_id': 'SolidFlex2000-87654321_6002', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_soc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'CMS-SF2000 Battery SOC', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_soc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_total_charging_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_battery_total_charging_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery total charging energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery total charging energy', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_total_charging_energy', + 'unique_id': 'SolidFlex2000-87654321_6006', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_total_charging_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'CMS-SF2000 Battery total charging energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_total_charging_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '380.58', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_total_discharging_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_battery_total_discharging_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery total discharging energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery total discharging energy', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_total_discharging_energy', + 'unique_id': 'SolidFlex2000-87654321_6007', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_battery_total_discharging_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'CMS-SF2000 Battery total discharging energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_battery_total_discharging_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '338.07', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_bypass_input_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_bypass_input_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Bypass input energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Bypass input energy', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass_input_energy', + 'unique_id': 'SolidFlex2000-87654321_11034', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_bypass_input_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'CMS-SF2000 Bypass input energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_bypass_input_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_bypass_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_bypass_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Bypass power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Bypass power', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass_power', + 'unique_id': 'SolidFlex2000-87654321_667', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_bypass_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'CMS-SF2000 Bypass power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_bypass_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_daily_production-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_daily_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Daily production', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daily production', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production', + 'unique_id': 'SolidFlex2000-87654321_1502', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_daily_production-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'CMS-SF2000 Daily production', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_daily_production', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_current_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_current_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input current 1', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input current 1', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_current_1', + 'unique_id': 'SolidFlex2000-87654321_1632', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_current_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'CMS-SF2000 DC input current 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_current_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.2', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_current_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_current_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input current 2', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input current 2', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_current_2', + 'unique_id': 'SolidFlex2000-87654321_1633', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_current_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'CMS-SF2000 DC input current 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_current_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.1', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_current_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_current_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input current 3', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input current 3', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_current_3', + 'unique_id': 'SolidFlex2000-87654321_1634', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_current_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'CMS-SF2000 DC input current 3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_current_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.8', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_current_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_current_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input current 4', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input current 4', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_current_4', + 'unique_id': 'SolidFlex2000-87654321_1635', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_current_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'CMS-SF2000 DC input current 4', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_current_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.9', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_power_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_power_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input power 1', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input power 1', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_power_1', + 'unique_id': 'SolidFlex2000-87654321_1664', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_power_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'CMS-SF2000 DC input power 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_power_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_power_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_power_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input power 2', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input power 2', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_power_2', + 'unique_id': 'SolidFlex2000-87654321_1665', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_power_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'CMS-SF2000 DC input power 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_power_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_power_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_power_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input power 3', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input power 3', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_power_3', + 'unique_id': 'SolidFlex2000-87654321_1666', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_power_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'CMS-SF2000 DC input power 3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_power_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_power_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_power_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input power 4', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input power 4', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_power_4', + 'unique_id': 'SolidFlex2000-87654321_1667', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_power_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'CMS-SF2000 DC input power 4', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_power_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_voltage_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_voltage_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input voltage 1', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input voltage 1', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_voltage_1', + 'unique_id': 'SolidFlex2000-87654321_1600', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_voltage_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'CMS-SF2000 DC input voltage 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_voltage_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '48.5', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_voltage_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_voltage_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input voltage 2', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input voltage 2', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_voltage_2', + 'unique_id': 'SolidFlex2000-87654321_1601', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_voltage_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'CMS-SF2000 DC input voltage 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_voltage_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '48.3', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_voltage_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_voltage_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input voltage 3', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input voltage 3', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_voltage_3', + 'unique_id': 'SolidFlex2000-87654321_1602', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_voltage_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'CMS-SF2000 DC input voltage 3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_voltage_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '48.7', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_voltage_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_input_voltage_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC input voltage 4', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input voltage 4', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_input_voltage_4', + 'unique_id': 'SolidFlex2000-87654321_1603', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_input_voltage_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'CMS-SF2000 DC input voltage 4', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_input_voltage_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '48.6', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_output_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_dc_output_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DC output power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC output power', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dc_output_power', + 'unique_id': 'SolidFlex2000-87654321_1501', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_dc_output_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'CMS-SF2000 DC output power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_dc_output_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_device_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'main', + 'standalone', + 'sub', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_device_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Device mode', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Device mode', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mode', + 'unique_id': 'SolidFlex2000-87654321_606', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_device_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'CMS-SF2000 Device mode', + 'options': list([ + 'main', + 'standalone', + 'sub', + ]), + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_device_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'sub', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_energy_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'charge_discharge_schedule', + 'outdoor_portable', + 'real_time_control', + 'self_consumed_prioritized', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_energy_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Energy mode', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy mode', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_mode', + 'unique_id': 'SolidFlex2000-87654321_7101', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_energy_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'CMS-SF2000 Energy mode', + 'options': list([ + 'charge_discharge_schedule', + 'outdoor_portable', + 'real_time_control', + 'self_consumed_prioritized', + ]), + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_energy_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'self_consumed_prioritized', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_grid_frequency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_grid_frequency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Grid frequency', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid frequency', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'grid_frequency', + 'unique_id': 'SolidFlex2000-87654321_2612', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_grid_frequency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'frequency', + 'friendly_name': 'CMS-SF2000 Grid frequency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_grid_frequency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50.0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_grid_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_grid_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Grid voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid voltage', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'grid_voltage', + 'unique_id': 'SolidFlex2000-87654321_2600', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_grid_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'CMS-SF2000 Grid voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_grid_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1200', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_main_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_main_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Main current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Main current', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'main_current', + 'unique_id': 'SolidFlex2000-87654321_9013', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_main_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'CMS-SF2000 Main current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_main_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.2', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_main_serial_number-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_main_serial_number', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Main serial number', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Main serial number', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'main_serial_number', + 'unique_id': 'SolidFlex2000-87654321_9008', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_main_serial_number-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Main serial number', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_main_serial_number', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'MASTER-12345678', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_main_soc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_main_soc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Main SOC', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Main SOC', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'main_soc', + 'unique_id': 'SolidFlex2000-87654321_9000', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_main_soc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'CMS-SF2000 Main SOC', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_main_soc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_main_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_main_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Main temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Main temperature', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'main_temperature', + 'unique_id': 'SolidFlex2000-87654321_9012', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_main_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'CMS-SF2000 Main temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_main_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.5', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_main_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.cms_sf2000_main_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Main voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Main voltage', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'main_voltage', + 'unique_id': 'SolidFlex2000-87654321_9004', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_main_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'CMS-SF2000 Main voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_main_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '51.2', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_meter_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_meter_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Meter power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Meter power', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'meter_power', + 'unique_id': 'SolidFlex2000-87654321_11016', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_meter_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'CMS-SF2000 Meter power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_meter_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_off_grid_output_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_off_grid_output_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Off-grid output energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Off-grid output energy', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'off_grid_output_energy', + 'unique_id': 'SolidFlex2000-87654321_2105', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_off_grid_output_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'CMS-SF2000 Off-grid output energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_off_grid_output_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2000', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_rated_capacity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_rated_capacity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Rated capacity', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Rated capacity', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'rated_capacity', + 'unique_id': 'SolidFlex2000-87654321_142', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_rated_capacity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'CMS-SF2000 Rated capacity', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_rated_capacity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.79', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_total_ac_input_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_total_ac_input_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total AC input energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total AC input energy', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_ac_input_energy', + 'unique_id': 'SolidFlex2000-87654321_2107', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_total_ac_input_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'CMS-SF2000 Total AC input energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_total_ac_input_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '289.97', + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_total_ac_output_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cms_sf2000_total_ac_output_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total AC output energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total AC output energy', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_ac_output_energy', + 'unique_id': 'SolidFlex2000-87654321_2104', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[2][sensor.cms_sf2000_total_ac_output_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'CMS-SF2000 Total AC output energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.cms_sf2000_total_ac_output_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1500', + }) +# --- diff --git a/tests/components/indevolt/test_config_flow.py b/tests/components/indevolt/test_config_flow.py new file mode 100644 index 00000000000000..3a69e2493c69a1 --- /dev/null +++ b/tests/components/indevolt/test_config_flow.py @@ -0,0 +1,106 @@ +"""Tests the Indevolt config flow.""" + +from unittest.mock import AsyncMock + +from aiohttp import ClientError +import pytest + +from homeassistant.components.indevolt.const import ( + CONF_GENERATION, + CONF_SERIAL_NUMBER, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_MODEL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import TEST_DEVICE_SN_GEN2, TEST_HOST + +from tests.common import MockConfigEntry + + +async def test_user_flow_success( + hass: HomeAssistant, mock_indevolt: AsyncMock, mock_setup_entry: AsyncMock +) -> None: + """Test successful user-initiated config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": TEST_HOST} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "INDEVOLT CMS-SF2000" + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_SERIAL_NUMBER: TEST_DEVICE_SN_GEN2, + CONF_MODEL: "CMS-SF2000", + CONF_GENERATION: 2, + } + assert result["result"].unique_id == TEST_DEVICE_SN_GEN2 + + +@pytest.mark.parametrize( + ("exception", "expected_error"), + [ + (TimeoutError, "timeout"), + (ConnectionError, "cannot_connect"), + (ClientError, "cannot_connect"), + (Exception("Some unknown error"), "unknown"), + ], +) +async def test_user_flow_error( + hass: HomeAssistant, + mock_indevolt: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + expected_error: str, +) -> None: + """Test connection errors in user flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + # Configure mock to raise exception + mock_indevolt.get_config.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"]["base"] == expected_error + + # Test recovery by patching the library to work + mock_indevolt.get_config.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "INDEVOLT CMS-SF2000" + + +async def test_user_flow_duplicate_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_indevolt: AsyncMock +) -> None: + """Test duplicate entry aborts the flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + # Test duplicate entry creation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/indevolt/test_init.py b/tests/components/indevolt/test_init.py new file mode 100644 index 00000000000000..05670fea426b58 --- /dev/null +++ b/tests/components/indevolt/test_init.py @@ -0,0 +1,47 @@ +"""Tests for the Indevolt integration initialization and services.""" + +from unittest.mock import AsyncMock + +from indevolt_api import TimeOutException +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("generation", [2], indirect=True) +async def test_load_unload( + hass: HomeAssistant, mock_indevolt: AsyncMock, mock_config_entry: MockConfigEntry +) -> None: + """Test setting up and removing a config entry.""" + await setup_integration(hass, mock_config_entry) + + # Verify the config entry is successfully loaded + assert mock_config_entry.state is ConfigEntryState.LOADED + + # Unload the integration + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Verify the config entry is properly unloaded + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize("generation", [2], indirect=True) +async def test_load_failure( + hass: HomeAssistant, mock_indevolt: AsyncMock, mock_config_entry: MockConfigEntry +) -> None: + """Test setup failure when coordinator update fails.""" + # Simulate timeout error during coordinator initialization + mock_indevolt.get_config.side_effect = TimeOutException + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Verify the config entry enters retry state due to failure + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/indevolt/test_sensor.py b/tests/components/indevolt/test_sensor.py new file mode 100644 index 00000000000000..21aef8cffcdda9 --- /dev/null +++ b/tests/components/indevolt/test_sensor.py @@ -0,0 +1,164 @@ +"""Tests for the Indevolt sensor platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.indevolt.coordinator import SCAN_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("generation", [2, 1], indirect=True) +async def test_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_indevolt: AsyncMock, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, +) -> None: + """Test sensor registration for sensors.""" + with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize("generation", [2], indirect=True) +async def test_sensor_availability( + hass: HomeAssistant, + mock_indevolt: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor availability / non-availability.""" + with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get("sensor.cms_sf2000_battery_soc")) + assert state.state == "92" + + mock_indevolt.fetch_data.side_effect = ConnectionError + freezer.tick(delta=timedelta(seconds=SCAN_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get("sensor.cms_sf2000_battery_soc")) + assert state.state == STATE_UNAVAILABLE + + +# In individual tests, you can override the mock behavior +async def test_battery_pack_filtering( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_indevolt: AsyncMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test that battery pack sensors are filtered based on SN availability.""" + + # Mock battery pack data - only first two packs have SNs + mock_indevolt.fetch_data.return_value = { + "9032": "BAT001", + "9051": "BAT002", + "9070": None, + "9165": "", + "9218": None, + } + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Get all sensor entities + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + + # Verify sensors for packs 1 and 2 exist (with SNs) + pack1_sensors = [ + e + for e in entity_entries + if any(key in e.unique_id for key in ("9032", "9016", "9030", "9020", "19173")) + ] + pack2_sensors = [ + e + for e in entity_entries + if any(key in e.unique_id for key in ("9051", "9035", "9049", "9039", "19174")) + ] + + assert len(pack1_sensors) == 5 + assert len(pack2_sensors) == 5 + + # Verify sensors for packs 3, 4, and 5 don't exist (no SNs) + pack3_sensors = [ + e + for e in entity_entries + if any(key in e.unique_id for key in ("9070", "9054", "9068", "9058", "19175")) + ] + pack4_sensors = [ + e + for e in entity_entries + if any(key in e.unique_id for key in ("9165", "9149", "9163", "9153", "19176")) + ] + pack5_sensors = [ + e + for e in entity_entries + if any(key in e.unique_id for key in ("9218", "9202", "9216", "9206", "19177")) + ] + + assert len(pack3_sensors) == 0 + assert len(pack4_sensors) == 0 + assert len(pack5_sensors) == 0 + + +async def test_battery_pack_filtering_fetch_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_indevolt: AsyncMock, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test battery pack filtering when fetch fails.""" + + # Mock fetch_data to raise error on battery pack SN fetch + mock_indevolt.fetch_data.side_effect = HomeAssistantError("Timeout") + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Get all sensor entities + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + + # Verify sensors (no sensors) + battery_pack_keys = [ + "9032", + "9051", + "9070", + "9165", + "9218", + "9016", + "9035", + "9054", + "9149", + "9202", + ] + battery_sensors = [ + e + for e in entity_entries + if any(key in e.unique_id for key in battery_pack_keys) + ] + + assert len(battery_sensors) == 0 From 049a91049484ebeda743dbe91f141fb6424175ab Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:55:39 +0100 Subject: [PATCH 31/36] Fix frontend development PR download cache (#162928) --- .../components/frontend/pr_download.py | 31 ++++++++++--------- tests/components/frontend/conftest.py | 5 ++- tests/components/frontend/test_pr_download.py | 31 ++++++++++++++----- 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/frontend/pr_download.py b/homeassistant/components/frontend/pr_download.py index 4de28d7c405c8a..1d4c28a047151b 100644 --- a/homeassistant/components/frontend/pr_download.py +++ b/homeassistant/components/frontend/pr_download.py @@ -43,13 +43,13 @@ ) -async def _get_pr_head_sha(client: GitHubAPI, pr_number: int) -> str: - """Get the head SHA for the PR.""" +async def _get_pr_shas(client: GitHubAPI, pr_number: int) -> tuple[str, str]: + """Get the head and base SHAs for a PR.""" try: response = await client.generic( endpoint=f"/repos/home-assistant/frontend/pulls/{pr_number}", ) - return str(response.data["head"]["sha"]) + return str(response.data["head"]["sha"]), str(response.data["base"]["sha"]) except GitHubAuthenticationException as err: raise HomeAssistantError(ERROR_INVALID_TOKEN) from err except (GitHubRatelimitException, GitHubPermissionException) as err: @@ -137,9 +137,9 @@ async def _download_artifact_data( def _extract_artifact( artifact_data: bytes, cache_dir: pathlib.Path, - head_sha: str, + cache_key: str, ) -> None: - """Extract artifact and save SHA (runs in executor).""" + """Extract artifact and save cache key (runs in executor).""" frontend_dir = cache_dir / "hass_frontend" if cache_dir.exists(): @@ -163,9 +163,8 @@ def _extract_artifact( ) zip_file.extractall(str(frontend_dir)) - # Save the commit SHA for cache validation sha_file = cache_dir / ".sha" - sha_file.write_text(head_sha) + sha_file.write_text(cache_key) async def download_pr_artifact( @@ -186,27 +185,29 @@ async def download_pr_artifact( client = GitHubAPI(token=github_token, session=session) - head_sha = await _get_pr_head_sha(client, pr_number) + head_sha, base_sha = await _get_pr_shas(client, pr_number) + cache_key = f"{head_sha}:{base_sha}" frontend_dir = tmp_dir / "hass_frontend" sha_file = tmp_dir / ".sha" if frontend_dir.exists() and sha_file.exists(): try: - cached_sha = await hass.async_add_executor_job(sha_file.read_text) - if cached_sha.strip() == head_sha: + cached_key = await hass.async_add_executor_job(sha_file.read_text) + cached_key = cached_key.strip() + if cached_key == cache_key: _LOGGER.info( "Using cached PR #%s (commit %s) from %s", pr_number, - head_sha[:8], + cache_key, tmp_dir, ) return tmp_dir _LOGGER.info( - "PR #%s has new commits (cached: %s, current: %s), re-downloading", + "PR #%s cache outdated (cached: %s, current: %s), re-downloading", pr_number, - cached_sha[:8], - head_sha[:8], + cached_key, + cache_key, ) except OSError as err: _LOGGER.debug("Failed to read cache SHA file: %s", err) @@ -218,7 +219,7 @@ async def download_pr_artifact( try: await hass.async_add_executor_job( - _extract_artifact, artifact_data, tmp_dir, head_sha + _extract_artifact, artifact_data, tmp_dir, cache_key ) except zipfile.BadZipFile as err: raise HomeAssistantError( diff --git a/tests/components/frontend/conftest.py b/tests/components/frontend/conftest.py index 7ec108a316b4a3..5191d6bbfa6d13 100644 --- a/tests/components/frontend/conftest.py +++ b/tests/components/frontend/conftest.py @@ -19,7 +19,10 @@ def mock_github_api() -> Generator[AsyncMock]: # Mock PR response pr_response = AsyncMock() - pr_response.data = {"head": {"sha": "abc123def456"}} + pr_response.data = { + "head": {"sha": "abc123def456"}, + "base": {"sha": "base789abc012"}, + } # Mock workflow runs response workflow_response = AsyncMock() diff --git a/tests/components/frontend/test_pr_download.py b/tests/components/frontend/test_pr_download.py index 0af85a66a153a4..352040917b156e 100644 --- a/tests/components/frontend/test_pr_download.py +++ b/tests/components/frontend/test_pr_download.py @@ -64,7 +64,7 @@ async def test_pr_download_uses_cache( frontend_dir = pr_cache_dir / "hass_frontend" frontend_dir.mkdir(parents=True) (frontend_dir / "index.html").write_text("test") - (pr_cache_dir / ".sha").write_text("abc123def456") + (pr_cache_dir / ".sha").write_text("abc123def456:base789abc012") with patch( "homeassistant.components.frontend.pr_download.GitHubAPI" @@ -73,7 +73,10 @@ async def test_pr_download_uses_cache( mock_gh_class.return_value = mock_client pr_response = AsyncMock() - pr_response.data = {"head": {"sha": "abc123def456"}} + pr_response.data = { + "head": {"sha": "abc123def456"}, + "base": {"sha": "base789abc012"}, + } mock_client.generic.return_value = pr_response config = { @@ -93,21 +96,29 @@ async def test_pr_download_uses_cache( assert "pulls" in str(calls[0]) +@pytest.mark.parametrize( + ("cache_key"), + [ + ("old_head_sha:base789abc012"), + ("abc123def456:old_base_sha"), + ], +) async def test_pr_download_cache_invalidated( hass: HomeAssistant, tmp_path: Path, mock_github_api, aioclient_mock: AiohttpClientMocker, mock_zipfile, + cache_key: str, ) -> None: - """Test that cache is invalidated when commit changes.""" + """Test that cache is invalidated when head commit changes.""" hass.config.config_dir = str(tmp_path) pr_cache_dir = tmp_path / ".cache" / "frontend" / "development_artifacts" frontend_dir = pr_cache_dir / "hass_frontend" frontend_dir.mkdir(parents=True) (frontend_dir / "index.html").write_text("test") - (pr_cache_dir / ".sha").write_text("old_commit_sha") + (pr_cache_dir / ".sha").write_text(cache_key) aioclient_mock.get( "https://api.github.com/artifact/download", @@ -124,7 +135,7 @@ async def test_pr_download_cache_invalidated( assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - # Should download - commit changed + # Should download - head commit changed assert len(aioclient_mock.mock_calls) == 1 @@ -261,7 +272,10 @@ async def test_pr_download_artifact_search_github_errors( mock_gh_class.return_value = mock_client pr_response = AsyncMock() - pr_response.data = {"head": {"sha": "abc123def456"}} + pr_response.data = { + "head": {"sha": "abc123def456"}, + "base": {"sha": "base789abc012"}, + } async def generic_side_effect(endpoint, **_kwargs): if "pulls" in endpoint: @@ -299,7 +313,10 @@ async def test_pr_download_artifact_not_found( mock_gh_class.return_value = mock_client pr_response = AsyncMock() - pr_response.data = {"head": {"sha": "abc123def456"}} + pr_response.data = { + "head": {"sha": "abc123def456"}, + "base": {"sha": "base789abc012"}, + } workflow_response = AsyncMock() workflow_response.data = {"workflow_runs": []} From 1d41e246535a25272624acb7f9aa3e5589c579f5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:00:18 +0100 Subject: [PATCH 32/36] Add type hints to extra_state_attributes [a-l] (#163279) --- homeassistant/components/directv/media_player.py | 2 +- homeassistant/components/discogs/sensor.py | 3 ++- homeassistant/components/dovado/sensor.py | 3 ++- homeassistant/components/environment_canada/sensor.py | 2 +- homeassistant/components/envisalink/sensor.py | 2 +- homeassistant/components/fail2ban/sensor.py | 3 ++- homeassistant/components/fido/sensor.py | 3 ++- homeassistant/components/flo/binary_sensor.py | 4 +++- homeassistant/components/geonetnz_quakes/sensor.py | 3 ++- homeassistant/components/geonetnz_volcano/sensor.py | 3 ++- homeassistant/components/gitter/sensor.py | 3 ++- homeassistant/components/gogogate2/entity.py | 4 +++- homeassistant/components/gogogate2/sensor.py | 3 ++- homeassistant/components/hdmi_cec/entity.py | 4 +++- homeassistant/components/homematic/entity.py | 4 ++-- homeassistant/components/hue/v1/binary_sensor.py | 4 +++- homeassistant/components/hue/v1/light.py | 3 ++- homeassistant/components/hue/v1/sensor.py | 4 +++- homeassistant/components/hue/v1/sensor_base.py | 2 +- homeassistant/components/ihc/entity.py | 3 ++- homeassistant/components/input_datetime/__init__.py | 4 ++-- homeassistant/components/input_number/__init__.py | 4 ++-- homeassistant/components/insteon/climate.py | 2 +- homeassistant/components/insteon/entity.py | 3 ++- homeassistant/components/intesishome/climate.py | 2 +- homeassistant/components/iperf3/sensor.py | 4 +++- homeassistant/components/kaiterra/air_quality.py | 4 +++- homeassistant/components/keenetic_ndms2/device_tracker.py | 3 ++- homeassistant/components/kef/media_player.py | 3 ++- homeassistant/components/linux_battery/sensor.py | 3 ++- homeassistant/components/london_air/sensor.py | 3 ++- homeassistant/components/lutron_caseta/binary_sensor.py | 4 +++- homeassistant/components/lutron_caseta/entity.py | 2 +- 33 files changed, 67 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 91934a2da3a912..6f57375e8781bf 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -117,7 +117,7 @@ async def async_update(self) -> None: self._attr_assumed_state = self._is_recorded @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" if self._is_standby: return {} diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 3c64b9020c33d4..cce4b5651db88c 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -5,6 +5,7 @@ from datetime import timedelta import logging import random +from typing import Any import discogs_client import voluptuous as vol @@ -118,7 +119,7 @@ def __init__( self._attr_name = f"{name} {description.name}" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the device state attributes of the sensor.""" if self._attr_native_value is None or self._attrs is None: return None diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index 0129b990435239..06a2e935d79b67 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from datetime import timedelta import re +from typing import Any import voluptuous as vol @@ -138,6 +139,6 @@ def update(self) -> None: self._attr_native_value = self._compute_state() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return {k: v for k, v in self._data.state.items() if k not in ["date", "time"]} diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index d27da132a35704..75d60ef16de92f 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -322,7 +322,7 @@ class ECAlertSensorEntity(ECBaseSensorEntity[ECWeather]): """Environment Canada sensor for alerts.""" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the extra state attributes.""" value = self.entity_description.value_fn(self._ec_data) if not value: diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py index d9b9ccab1640c4..4c445a76a8505f 100644 --- a/homeassistant/components/envisalink/sensor.py +++ b/homeassistant/components/envisalink/sensor.py @@ -89,7 +89,7 @@ def native_value(self): return self._info["status"]["alpha"] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return self._info["status"] diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index e4b6a1e90ee1cf..aa29f28244bd45 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -6,6 +6,7 @@ import logging import os import re +from typing import Any import voluptuous as vol @@ -76,7 +77,7 @@ def name(self): return self._name @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the fail2ban sensor.""" return self.ban_dict diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 86e81a596d7346..cbce2efd7c5e3a 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -8,6 +8,7 @@ from datetime import timedelta import logging +from typing import Any from pyfido import FidoClient from pyfido.client import PyFidoError @@ -226,7 +227,7 @@ def __init__( self._attr_name = f"{name} {number} {description.name}" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" return {"number": self._number} diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py index 89f317fd3c617c..8ac7991527e095 100644 --- a/homeassistant/components/flo/binary_sensor.py +++ b/homeassistant/components/flo/binary_sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -54,7 +56,7 @@ def is_on(self): return self._device.has_alerts @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" if not self._device.has_alerts: return {} diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py index cc4b4e16282834..ea2e4e9ff45e48 100644 --- a/homeassistant/components/geonetnz_quakes/sensor.py +++ b/homeassistant/components/geonetnz_quakes/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.core import HomeAssistant, callback @@ -136,7 +137,7 @@ def native_unit_of_measurement(self): return DEFAULT_UNIT_OF_MEASUREMENT @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" return { key: value diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index 159806778ce418..c55cbd76615b4f 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, UnitOfLength @@ -152,7 +153,7 @@ def native_unit_of_measurement(self): return "alert level" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" return { key: value diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index 957ac4e9d8c648..950dc319da4608 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import Any from gitterpy.client import GitterClient from gitterpy.errors import GitterRoomError, GitterTokenError @@ -90,7 +91,7 @@ def native_unit_of_measurement(self): return self._unit_of_measurement @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { ATTR_USERNAME: self._username, diff --git a/homeassistant/components/gogogate2/entity.py b/homeassistant/components/gogogate2/entity.py index a6879f038bc01d..f82e4d1f150028 100644 --- a/homeassistant/components/gogogate2/entity.py +++ b/homeassistant/components/gogogate2/entity.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from ismartgate.common import AbstractDoor, get_door_by_id from homeassistant.const import CONF_IP_ADDRESS @@ -62,6 +64,6 @@ def device_info(self) -> DeviceInfo: ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return {"door_id": self._door_id} diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index c594671b34f8ad..4e4fa908b8f1a9 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from itertools import chain +from typing import Any from ismartgate.common import AbstractDoor, get_configured_doors @@ -49,7 +50,7 @@ class DoorSensorEntity(GoGoGate2Entity, SensorEntity): """Base class for door sensor entities.""" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" attrs = super().extra_state_attributes door = self.door diff --git a/homeassistant/components/hdmi_cec/entity.py b/homeassistant/components/hdmi_cec/entity.py index 60ea4e1a0d0774..cc10fd95531bef 100644 --- a/homeassistant/components/hdmi_cec/entity.py +++ b/homeassistant/components/hdmi_cec/entity.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from homeassistant.helpers.entity import Entity from .const import DOMAIN, EVENT_HDMI_CEC_UNAVAILABLE @@ -95,7 +97,7 @@ def type_id(self): return self._device.type @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" state_attr = {} if self.vendor_id is not None: diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index 3e4d6a6fc71541..4cba934f3b1922 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -83,7 +83,7 @@ def available(self) -> bool: return self._available @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" # Static attributes attr = { @@ -240,7 +240,7 @@ def state(self): return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return self._variables.copy() diff --git a/homeassistant/components/hue/v1/binary_sensor.py b/homeassistant/components/hue/v1/binary_sensor.py index e06d61210b8a57..7000c16b52b0a8 100644 --- a/homeassistant/components/hue/v1/binary_sensor.py +++ b/homeassistant/components/hue/v1/binary_sensor.py @@ -1,5 +1,7 @@ """Hue binary sensor entities.""" +from typing import Any + from aiohue.v1.sensors import TYPE_ZLL_PRESENCE from homeassistant.components.binary_sensor import ( @@ -43,7 +45,7 @@ def is_on(self): return self.sensor.presence @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = super().extra_state_attributes if "sensitivity" in self.sensor.config: diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index f5da1ffb762b8f..4d07a2e9bbdad4 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -7,6 +7,7 @@ from functools import partial import logging import random +from typing import Any import aiohue @@ -622,7 +623,7 @@ async def async_turn_off(self, **kwargs): await self.coordinator.async_request_refresh() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" if not self.is_group: return {} diff --git a/homeassistant/components/hue/v1/sensor.py b/homeassistant/components/hue/v1/sensor.py index 765808bdf18a05..1f8ad7b1f9a938 100644 --- a/homeassistant/components/hue/v1/sensor.py +++ b/homeassistant/components/hue/v1/sensor.py @@ -1,5 +1,7 @@ """Hue sensor entities.""" +from typing import Any + from aiohue.v1.sensors import ( TYPE_ZLL_LIGHTLEVEL, TYPE_ZLL_ROTARY, @@ -64,7 +66,7 @@ def native_value(self): return round(float(10 ** ((self.sensor.lightlevel - 1) / 10000)), 2) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = super().extra_state_attributes attributes.update( diff --git a/homeassistant/components/hue/v1/sensor_base.py b/homeassistant/components/hue/v1/sensor_base.py index 0ea079992e004c..9cb836386e0893 100644 --- a/homeassistant/components/hue/v1/sensor_base.py +++ b/homeassistant/components/hue/v1/sensor_base.py @@ -206,6 +206,6 @@ class GenericZLLSensor(GenericHueSensor): """Representation of a Hue-brand, physical sensor.""" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" return {"battery_level": self.sensor.battery} diff --git a/homeassistant/components/ihc/entity.py b/homeassistant/components/ihc/entity.py index 8847ffc9f492c2..b2138eb8aab2e7 100644 --- a/homeassistant/components/ihc/entity.py +++ b/homeassistant/components/ihc/entity.py @@ -1,6 +1,7 @@ """Implementation of a base class for all IHC devices.""" import logging +from typing import Any from ihcsdk.ihccontroller import IHCController @@ -70,7 +71,7 @@ def unique_id(self): return f"{self.controller_id}-{self.ihc_id}" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" if not self.hass.data[DOMAIN][self.controller_id][CONF_INFO]: return {} diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 6deab74078a893..ba183090277c7c 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -337,9 +337,9 @@ def capability_attributes(self) -> dict[str, Any]: } @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - attrs = { + attrs: dict[str, Any] = { ATTR_EDITABLE: self.editable, } diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 9049f273a67d56..8d5cf877f8af44 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -4,7 +4,7 @@ from contextlib import suppress import logging -from typing import Self +from typing import Any, Self import voluptuous as vol @@ -268,7 +268,7 @@ def unique_id(self) -> str | None: return self._config[CONF_ID] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { ATTR_INITIAL: self._config.get(CONF_INITIAL), diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index eb33e3ab88c16a..e26d30d5cdd049 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -168,7 +168,7 @@ def hvac_action(self) -> HVACAction: return HVACAction.IDLE @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Provide attributes for display on device card.""" attr = super().extra_state_attributes humidifier = "off" diff --git a/homeassistant/components/insteon/entity.py b/homeassistant/components/insteon/entity.py index 0b2bbbf9e2e712..894596b6a063e2 100644 --- a/homeassistant/components/insteon/entity.py +++ b/homeassistant/components/insteon/entity.py @@ -2,6 +2,7 @@ import functools import logging +from typing import Any from pyinsteon import devices @@ -72,7 +73,7 @@ def name(self): return f"{description} {self._insteon_device.address}{extension}" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Provide attributes for display on device card.""" return { "insteon_address": self.address, diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 3465a7e5c07706..43a23e39676ddd 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -218,7 +218,7 @@ async def async_added_to_hass(self) -> None: raise PlatformNotReady from ex @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" attrs = {} if self._outdoor_temp: diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py index 9ba3b55ed4f3d1..b30e019798c83a 100644 --- a/homeassistant/components/iperf3/sensor.py +++ b/homeassistant/components/iperf3/sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant, callback @@ -50,7 +52,7 @@ def __init__(self, iperf3_data, description: SensorEntityDescription) -> None: self._attr_name = f"{description.name} {iperf3_data.host}" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { ATTR_PROTOCOL: self._iperf3_data.protocol, diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py index 97553d6bda6f2c..cdd9f3461ce2a2 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from homeassistant.components.air_quality import AirQualityEntity from homeassistant.const import CONF_DEVICE_ID, CONF_NAME from homeassistant.core import HomeAssistant @@ -104,7 +106,7 @@ def unique_id(self): return f"{self._device_id}_air_quality" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" return { attr: value diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index 7de7c497ef334d..94cdb13d79eb06 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import Any from ndms2_client import Device @@ -126,7 +127,7 @@ def available(self) -> bool: return self._router.available @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the device state attributes.""" if self.is_connected: return { diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 1c5188b1a6f399..c5f350e00cd96a 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -6,6 +6,7 @@ from functools import partial import ipaddress import logging +from typing import Any from aiokef import AsyncKefSpeaker from aiokef.aiokef import DSP_OPTION_MAPPING @@ -346,7 +347,7 @@ async def async_will_remove_from_hass(self) -> None: self._update_dsp_task_remover = None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the DSP settings of the KEF device.""" return self._dsp or {} diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index fffb6357a285af..e5f7370eb5f2ac 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -4,6 +4,7 @@ import logging import os +from typing import Any from batinfo import Batteries import voluptuous as vol @@ -97,7 +98,7 @@ def __init__(self, name, battery_id, system): self._system = system @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" if self._system == "android": return { diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index b3c7535b9b7fb9..33b21473735cd1 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -5,6 +5,7 @@ from datetime import timedelta from http import HTTPStatus import logging +from typing import Any import requests import voluptuous as vol @@ -137,7 +138,7 @@ def icon(self): return self.ICON @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return other details about the sensor state.""" attrs = {} attrs["updated"] = self._updated diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 4a92eb5c3b7432..9db8dee0ac51d3 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -1,5 +1,7 @@ """Support for Lutron Caseta Occupancy/Vacancy Sensors.""" +from typing import Any + from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED from homeassistant.components.binary_sensor import ( @@ -83,6 +85,6 @@ def unique_id(self): return f"occupancygroup_{self._bridge_unique_id}_{self.device_id}" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return {"device_id": self.device_id} diff --git a/homeassistant/components/lutron_caseta/entity.py b/homeassistant/components/lutron_caseta/entity.py index 8cae22f5042ed7..cde2cb52923709 100644 --- a/homeassistant/components/lutron_caseta/entity.py +++ b/homeassistant/components/lutron_caseta/entity.py @@ -93,7 +93,7 @@ def unique_id(self) -> str: return str(self._handle_none_serial(self.serial)) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" attributes = { "device_id": self.device_id, From e7aa0ae39856203e66b1f06566120727e7e10c9b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:00:42 +0100 Subject: [PATCH 33/36] Add type hints to extra_state_attributes [m-z] (#163281) --- homeassistant/components/maxcube/climate.py | 2 +- homeassistant/components/motion_blinds/sensor.py | 6 ++++-- homeassistant/components/mqtt_room/sensor.py | 2 +- homeassistant/components/ohmconnect/sensor.py | 3 ++- homeassistant/components/pencom/switch.py | 2 +- homeassistant/components/plant/__init__.py | 3 ++- homeassistant/components/plex/media_player.py | 2 +- homeassistant/components/plex/sensor.py | 3 ++- homeassistant/components/point/entity.py | 3 ++- homeassistant/components/proliphix/climate.py | 2 +- homeassistant/components/push/camera.py | 4 ++-- homeassistant/components/qvr_pro/camera.py | 3 ++- homeassistant/components/rainbird/switch.py | 2 +- homeassistant/components/raincloud/entity.py | 4 +++- homeassistant/components/raincloud/switch.py | 2 +- homeassistant/components/reddit/sensor.py | 3 ++- homeassistant/components/rejseplanen/sensor.py | 3 ++- homeassistant/components/rmvtransport/sensor.py | 3 ++- homeassistant/components/smart_meter_texas/sensor.py | 4 +++- homeassistant/components/solaredge_local/sensor.py | 3 ++- homeassistant/components/soundtouch/media_player.py | 4 ++-- homeassistant/components/starline/sensor.py | 4 +++- homeassistant/components/supervisord/sensor.py | 3 ++- homeassistant/components/swiss_hydrological_data/sensor.py | 5 +++-- homeassistant/components/tellduslive/entity.py | 3 ++- homeassistant/components/tmb/sensor.py | 3 ++- homeassistant/components/travisci/sensor.py | 5 +++-- homeassistant/components/universal/media_player.py | 2 +- homeassistant/components/upb/entity.py | 4 +++- homeassistant/components/utility_meter/sensor.py | 2 +- homeassistant/components/verisure/binary_sensor.py | 4 +++- homeassistant/components/worldtidesinfo/sensor.py | 3 ++- homeassistant/components/xiaomi_miio/air_quality.py | 3 ++- homeassistant/components/zestimate/sensor.py | 3 ++- 34 files changed, 68 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index 862e811fb6f251..d24eace9b918f7 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -225,7 +225,7 @@ def set_preset_mode(self, preset_mode: str) -> None: raise ValueError(f"unsupported preset mode {preset_mode}") @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" if not self._device.is_thermostat(): return {} diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 60d283aa0b6332..eac89eccdd205f 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -1,5 +1,7 @@ """Support for Motionblinds sensors.""" +from typing import Any + from motionblinds import DEVICE_TYPES_WIFI from motionblinds.motion_blinds import DEVICE_TYPE_TDBU @@ -68,7 +70,7 @@ def native_value(self): return self._blind.battery_level @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage} @@ -92,7 +94,7 @@ def native_value(self): return self._blind.battery_level[self._motor[0]] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" attributes = {} if self._blind.battery_voltage is not None: diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 242c39cb98369c..10051bdeb16c02 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -173,7 +173,7 @@ def name(self): return self._name @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return {ATTR_DISTANCE: self._distance} diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 287842178d8b46..19000da21049d9 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging +from typing import Any import defusedxml.ElementTree as ET import requests @@ -70,7 +71,7 @@ def native_value(self): return "Inactive" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return {"Address": self._data.get("address"), "ID": self._ohmid} diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index d9d89494bd93ad..28e3671929184e 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -108,6 +108,6 @@ def update(self) -> None: self._state = self._hub.get(self._board, self._addr) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return supported attributes.""" return {"board": self._board, "addr": self._addr} diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 27993a93779916..77c1c2b7b6ce85 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -8,6 +8,7 @@ from contextlib import suppress from datetime import datetime, timedelta import logging +from typing import Any import voluptuous as vol @@ -345,7 +346,7 @@ def state(self): return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes of the entity. Provide the individual measurements from the diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index ed96adeff8ab7a..0c74714cb4e454 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -500,7 +500,7 @@ def play_media( ) from exc @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the scene state attributes.""" attributes = {} for attr in ( diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 66e513dd83aa48..87af46f198d6a0 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import Any from plexapi.exceptions import NotFound import requests.exceptions @@ -110,7 +111,7 @@ async def _async_refresh_sensor(self) -> None: self.async_write_ha_state() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return self._server.sensor_attributes diff --git a/homeassistant/components/point/entity.py b/homeassistant/components/point/entity.py index b6718d7fd2d355..bdf506d8d1f2d3 100644 --- a/homeassistant/components/point/entity.py +++ b/homeassistant/components/point/entity.py @@ -1,6 +1,7 @@ """Support for Minut Point.""" import logging +from typing import Any from pypoint import Device, PointSession @@ -56,7 +57,7 @@ def device(self) -> Device: return self.client.device(self.device_id) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return status of device.""" attrs = self.device.device_status attrs["last_heard_from"] = as_local( diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 03f53dec390536..847f6963e05cb0 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -78,7 +78,7 @@ def name(self): return self._name @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" return {ATTR_FAN: self._pdp.fan_state} diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 7c1d37712bb680..26c91bb6d29cb6 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -6,7 +6,7 @@ from collections import deque from datetime import timedelta import logging -from typing import cast +from typing import Any, cast from aiohttp import web import voluptuous as vol @@ -183,7 +183,7 @@ async def async_camera_image( return self._current_image @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { name: value diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py index 38221f89cfd35c..6496ce304a78e6 100644 --- a/homeassistant/components/qvr_pro/camera.py +++ b/homeassistant/components/qvr_pro/camera.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import Any from pyqvrpro.client import QVRResponseError @@ -88,7 +89,7 @@ def brand(self): return self._brand @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Get the state attributes.""" return {"qvr_guid": self.guid} diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 687de2a6d97d47..49d1cb68d2f64b 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -72,7 +72,7 @@ def __init__( ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return state attributes.""" return {"zone": self._zone} diff --git a/homeassistant/components/raincloud/entity.py b/homeassistant/components/raincloud/entity.py index b45684ac72b96c..43ff715fb12d6a 100644 --- a/homeassistant/components/raincloud/entity.py +++ b/homeassistant/components/raincloud/entity.py @@ -1,5 +1,7 @@ """Support for Melnor RainCloud sprinkler water timer.""" +from typing import Any + from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -58,7 +60,7 @@ def _update_callback(self): self.schedule_update_ha_state(True) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return {"identifier": self.data.serial} diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py index babadcba676f42..10134717bb5794 100644 --- a/homeassistant/components/raincloud/switch.py +++ b/homeassistant/components/raincloud/switch.py @@ -98,7 +98,7 @@ def update(self) -> None: self._state = self.data.auto_watering @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { "default_manual_timer": self._default_watering_timer, diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 564cc6c3c06dab..0f758d565fa56e 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging +from typing import Any import praw import voluptuous as vol @@ -118,7 +119,7 @@ def native_value(self): return len(self._subreddit_data) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { ATTR_SUBREDDIT: self._subreddit, diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 87e0947c78db9f..6265fffc7b6c30 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -10,6 +10,7 @@ from datetime import datetime, timedelta import logging from operator import itemgetter +from typing import Any import rjpl import voluptuous as vol @@ -124,7 +125,7 @@ def native_value(self): return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" if not self._times: return {ATTR_STOP_ID: self._stop_id} diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 114df787053992..4128831f8663ec 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -5,6 +5,7 @@ import asyncio from datetime import timedelta import logging +from typing import Any from RMVtransport import RMVtransport from RMVtransport.rmvtransport import ( @@ -166,7 +167,7 @@ def native_value(self): return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" try: return { diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py index 6099b489c4387e..ce1f30cd4cd3dd 100644 --- a/homeassistant/components/smart_meter_texas/sensor.py +++ b/homeassistant/components/smart_meter_texas/sensor.py @@ -1,5 +1,7 @@ """Support for Smart Meter Texas sensors.""" +from typing import Any + from smart_meter_texas import Meter from homeassistant.components.sensor import ( @@ -58,7 +60,7 @@ def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator) -> None: self._attr_unique_id = f"{meter.esiid}_{meter.meter}" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" return { METER_NUMBER: self.meter.meter, diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index d8621a139c0118..f362a5e029f20d 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -7,6 +7,7 @@ from datetime import timedelta import logging import statistics +from typing import Any from requests.exceptions import ConnectTimeout, HTTPError from solaredge_local import SolarEdge @@ -289,7 +290,7 @@ def __init__( self._attr_name = f"{platform_name} ({description.name})" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if extra_attr := self.entity_description.extra_attribute: try: diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index c540b8dfd6446b..02c0d8a1bbf9ff 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -333,9 +333,9 @@ def add_zone_slave(self, slaves): self._device.add_zone_slave([slave.device for slave in slaves]) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return entity specific state attributes.""" - attributes = {} + attributes: dict[str, Any] = {} if self._zone and "master" in self._zone: attributes[ATTR_SOUNDTOUCH_ZONE] = self._zone diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 7c3690837f1edb..fee189dbf3b141 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -166,7 +168,7 @@ def native_unit_of_measurement(self): return self.entity_description.native_unit_of_measurement @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the sensor.""" if self._key == "balance": return self._account.balance_attrs(self._device) diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index c14eb6fb353d43..555e44e7354b58 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import Any import xmlrpc.client import voluptuous as vol @@ -76,7 +77,7 @@ def available(self) -> bool: return self._available @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { ATTR_DESCRIPTION: self._info.get("description"), diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index 897b440a93496a..e475ae909d0616 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging +from typing import Any from swisshydrodata import SwissHydroData import voluptuous as vol @@ -125,9 +126,9 @@ def native_value(self): return None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" - attrs = {} + attrs: dict[str, Any] = {} if not self._data: return attrs diff --git a/homeassistant/components/tellduslive/entity.py b/homeassistant/components/tellduslive/entity.py index 5366e4c27dfa0a..35a733dd1b45c7 100644 --- a/homeassistant/components/tellduslive/entity.py +++ b/homeassistant/components/tellduslive/entity.py @@ -2,6 +2,7 @@ from datetime import datetime import logging +from typing import Any from tellduslive import BATTERY_LOW, BATTERY_OK, BATTERY_UNKNOWN @@ -68,7 +69,7 @@ def available(self) -> bool: return self._client.is_available(self.device_id) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" attrs = {} if self._battery_level: diff --git a/homeassistant/components/tmb/sensor.py b/homeassistant/components/tmb/sensor.py index cbf3b073578df4..0d9f6ff8fb2532 100644 --- a/homeassistant/components/tmb/sensor.py +++ b/homeassistant/components/tmb/sensor.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging +from typing import Any from requests import HTTPError from tmb import IBus @@ -108,7 +109,7 @@ def native_value(self): return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the last update.""" return { ATTR_BUS_STOP: self._stop, diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 8193c5a67dc74e..9644016b90a68a 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging +from typing import Any from travispy import TravisPy from travispy.errors import TravisError @@ -154,9 +155,9 @@ def __init__( self._attr_name = f"{repo_name} {description.name}" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - attrs = {} + attrs: dict[str, Any] = {} if self._build and self._attr_native_value is not None: if self._user and self.entity_description.key == "state": diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 332d52498d160b..0f9df0c10f330a 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -533,7 +533,7 @@ def supported_features(self) -> MediaPlayerEntityFeature: return flags @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" active_child = self._child_state return {ATTR_ACTIVE_CHILD: active_child.entity_id} if active_child else {} diff --git a/homeassistant/components/upb/entity.py b/homeassistant/components/upb/entity.py index 8a9afa453b1d2a..72d658c64ce400 100644 --- a/homeassistant/components/upb/entity.py +++ b/homeassistant/components/upb/entity.py @@ -1,5 +1,7 @@ """Support the UPB PIM.""" +from typing import Any + from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity @@ -25,7 +27,7 @@ def unique_id(self): return self._unique_id @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the default attributes of the element.""" return self._element.as_dict() diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index faa55ced255b8c..f7e6f6e3008235 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -702,7 +702,7 @@ def state_class(self) -> SensorStateClass: ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" state_attr = { ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING, diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 4d9221c3ca97c1..c42454b380a7f8 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -82,7 +84,7 @@ def available(self) -> bool: ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" return { ATTR_LAST_TRIP_TIME: dt_util.parse_datetime( diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index 1a64954bb4a358..b38b3d4f602ca0 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -5,6 +5,7 @@ from datetime import timedelta import logging import time +from typing import Any import requests import voluptuous as vol @@ -81,7 +82,7 @@ def name(self): return self._name @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of this device.""" attr = {} diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 9e52abb1c85442..95f29f6697c137 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -2,6 +2,7 @@ from collections.abc import Callable import logging +from typing import Any from miio import ( AirQualityMonitor, @@ -116,7 +117,7 @@ def humidity(self): return self._humidity @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" data = {} diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 6b3b38bdde859c..c776cce2ca0f85 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging +from typing import Any import requests import voluptuous as vol @@ -99,7 +100,7 @@ def native_value(self): return None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" attributes = {} if self.data is not None: From bd4523297210ed510fe480afa093af5aa6cb8033 Mon Sep 17 00:00:00 2001 From: theobld-ww <60600399+theobld-ww@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:07:53 +0100 Subject: [PATCH 34/36] Translation keys for exceptions Watts Vision + integration (#163231) Co-authored-by: Josef Zweck --- homeassistant/components/watts/__init__.py | 18 +++- homeassistant/components/watts/coordinator.py | 32 +++++-- .../components/watts/quality_scale.yaml | 2 +- homeassistant/components/watts/strings.json | 33 ++++++- tests/components/watts/test_init.py | 86 ++++++++++++++++++- 5 files changed, 156 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/watts/__init__.py b/homeassistant/components/watts/__init__.py index 18abe77fb4b458..0d4f08741e0461 100644 --- a/homeassistant/components/watts/__init__.py +++ b/homeassistant/components/watts/__init__.py @@ -95,7 +95,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: WattsVisionConfigEntry) ) except config_entry_oauth2_flow.ImplementationUnavailableError as err: raise ConfigEntryNotReady( - "OAuth2 implementation temporarily unavailable" + translation_domain=DOMAIN, + translation_key="oauth_implementation_unavailable", ) from err oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) @@ -104,10 +105,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: WattsVisionConfigEntry) await oauth_session.async_ensure_token_valid() except ClientResponseError as err: if HTTPStatus.BAD_REQUEST <= err.status < HTTPStatus.INTERNAL_SERVER_ERROR: - raise ConfigEntryAuthFailed("OAuth session not valid") from err - raise ConfigEntryNotReady("Temporary connection error") from err + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="oauth_session_not_valid", + ) from err + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="temporary_connection_error", + ) from err except ClientError as err: - raise ConfigEntryNotReady("Network issue during OAuth setup") from err + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="network_issue", + ) from err session = aiohttp_client.async_get_clientsession(hass) auth = WattsVisionAuth( diff --git a/homeassistant/components/watts/coordinator.py b/homeassistant/components/watts/coordinator.py index c619c8659c1aee..c24853eb52c74d 100644 --- a/homeassistant/components/watts/coordinator.py +++ b/homeassistant/components/watts/coordinator.py @@ -80,7 +80,10 @@ async def _async_update_data(self) -> dict[str, Device]: try: devices_list = await self.client.discover_devices() except WattsVisionAuthError as err: - raise ConfigEntryAuthFailed("Authentication failed") from err + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="authentication_failed", + ) from err except ( WattsVisionConnectionError, WattsVisionTimeoutError, @@ -91,7 +94,10 @@ async def _async_update_data(self) -> dict[str, Device]: ValueError, ) as err: if is_first_refresh: - raise ConfigEntryNotReady("Failed to discover devices") from err + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="failed_to_discover_devices", + ) from err _LOGGER.warning( "Periodic discovery failed: %s, falling back to update", err ) @@ -114,7 +120,10 @@ async def _async_update_data(self) -> dict[str, Device]: try: devices = await self.client.get_devices_report(device_ids) except WattsVisionAuthError as err: - raise ConfigEntryAuthFailed("Authentication failed") from err + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="authentication_failed", + ) from err except ( WattsVisionConnectionError, WattsVisionTimeoutError, @@ -124,7 +133,10 @@ async def _async_update_data(self) -> dict[str, Device]: TimeoutError, ValueError, ) as err: - raise UpdateFailed("Failed to update devices") from err + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="failed_to_update_devices", + ) from err _LOGGER.debug("Updated %d devices", len(devices)) return devices @@ -207,10 +219,18 @@ async def _async_update_data(self) -> WattsVisionDeviceData: TimeoutError, ValueError, ) as err: - raise UpdateFailed(f"Failed to refresh device {self.device_id}") from err + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="failed_to_refresh_device", + translation_placeholders={"device_id": self.device_id}, + ) from err if not device: - raise UpdateFailed(f"Device {self.device_id} not found") + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="device_not_found", + translation_placeholders={"device_id": self.device_id}, + ) _LOGGER.debug("Refreshed device %s", self.device_id) return WattsVisionDeviceData(device=device) diff --git a/homeassistant/components/watts/quality_scale.yaml b/homeassistant/components/watts/quality_scale.yaml index a0355892b152bc..349cd8ced16d3d 100644 --- a/homeassistant/components/watts/quality_scale.yaml +++ b/homeassistant/components/watts/quality_scale.yaml @@ -56,7 +56,7 @@ rules: entity-translations: status: exempt comment: No entity required translations. - exception-translations: todo + exception-translations: done icon-translations: status: exempt comment: Thermostat entities use standard HA Climate entity. diff --git a/homeassistant/components/watts/strings.json b/homeassistant/components/watts/strings.json index 4524f670e731f5..aeea7abfd83f8f 100644 --- a/homeassistant/components/watts/strings.json +++ b/homeassistant/components/watts/strings.json @@ -30,14 +30,41 @@ } }, "exceptions": { + "authentication_failed": { + "message": "Authentication failed" + }, + "device_not_found": { + "message": "Device {device_id} not found" + }, + "failed_to_discover_devices": { + "message": "Failed to discover devices" + }, + "failed_to_refresh_device": { + "message": "Failed to refresh device {device_id}" + }, + "failed_to_update_devices": { + "message": "Failed to update devices" + }, + "network_issue": { + "message": "Network issue during OAuth setup" + }, + "oauth_implementation_unavailable": { + "message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]" + }, + "oauth_session_not_valid": { + "message": "OAuth session not valid" + }, "set_hvac_mode_error": { - "message": "An error occurred while setting the HVAC mode." + "message": "An error occurred while setting the HVAC mode" }, "set_switch_state_error": { - "message": "An error occurred while setting the switch state." + "message": "An error occurred while setting the switch state" }, "set_temperature_error": { - "message": "An error occurred while setting the temperature." + "message": "An error occurred while setting the temperature" + }, + "temporary_connection_error": { + "message": "Temporary connection error" } } } diff --git a/tests/components/watts/test_init.py b/tests/components/watts/test_init.py index 98a85690972bd9..b5faeeab987522 100644 --- a/tests/components/watts/test_init.py +++ b/tests/components/watts/test_init.py @@ -15,12 +15,20 @@ ) from visionpluspython.models import create_device_from_data +from homeassistant.components.climate import ( + ATTR_TEMPERATURE, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, +) from homeassistant.components.watts.const import ( DISCOVERY_INTERVAL_MINUTES, DOMAIN, + FAST_POLLING_INTERVAL_SECONDS, OAUTH2_TOKEN, + UPDATE_INTERVAL_SECONDS, ) -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -259,6 +267,7 @@ async def test_stale_device_removal( assert device_456 is not None current_devices = list(mock_watts_client.discover_devices.return_value) + # remove thermostat_456 mock_watts_client.discover_devices.return_value = [ d for d in current_devices if d.device_id != "thermostat_456" @@ -273,3 +282,78 @@ async def test_stale_device_removal( identifiers={(DOMAIN, "thermostat_456")} ) assert device_456_after_removal is None + + +@pytest.mark.parametrize( + ("exception", "has_reauth_flow"), + [ + (WattsVisionAuthError("expired"), True), + (WattsVisionConnectionError("lost"), False), + ], +) +async def test_hub_coordinator_update_errors( + hass: HomeAssistant, + mock_watts_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + exception: Exception, + has_reauth_flow: bool, +) -> None: + """Test hub coordinator handles errors during regular update.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("climate.living_room_thermostat") + assert state is not None + assert state.state != STATE_UNAVAILABLE + + mock_watts_client.get_devices_report.side_effect = exception + + freezer.tick(timedelta(seconds=UPDATE_INTERVAL_SECONDS)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("climate.living_room_thermostat") + assert state is not None + assert state.state != STATE_UNAVAILABLE + + assert ( + any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) + == has_reauth_flow + ) + + +async def test_device_coordinator_refresh_error( + hass: HomeAssistant, + mock_watts_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test device coordinator handles refresh error.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("climate.living_room_thermostat") + assert state is not None + assert state.state != STATE_UNAVAILABLE + + # Activate fast polling + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.living_room_thermostat", + ATTR_TEMPERATURE: 23.5, + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Device refresh fail on the next fast poll + mock_watts_client.get_device.side_effect = WattsVisionConnectionError("lost") + + freezer.tick(timedelta(seconds=FAST_POLLING_INTERVAL_SECONDS)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("climate.living_room_thermostat") + assert state is not None + assert state.state == STATE_UNAVAILABLE From b44900532f3683e23cf9a2e6fee3e01c280edba1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:10:11 +0100 Subject: [PATCH 35/36] Ensure DOMAIN constant is always aliased with _DOMAIN suffix (#163270) --- homeassistant/components/hdmi_cec/__init__.py | 10 ++++++---- pylint/plugins/hass_imports.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 3a7f07081e2878..3f948a4474f789 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -24,7 +24,7 @@ import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -122,11 +122,13 @@ vol.Optional(CONF_DEVICES): vol.Any( DEVICE_SCHEMA, vol.Schema({vol.All(cv.string): vol.Any(cv.string)}) ), - vol.Optional(CONF_PLATFORM): vol.Any(SWITCH, MEDIA_PLAYER_DOMAIN), + vol.Optional(CONF_PLATFORM): vol.Any( + SWITCH_DOMAIN, MEDIA_PLAYER_DOMAIN + ), vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_DISPLAY_NAME): cv.string, vol.Optional(CONF_TYPES, default={}): vol.Schema( - {cv.entity_id: vol.Any(MEDIA_PLAYER_DOMAIN, SWITCH)} + {cv.entity_id: vol.Any(MEDIA_PLAYER_DOMAIN, SWITCH_DOMAIN)} ), } ) @@ -170,7 +172,7 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 device_aliases.update(parse_mapping(devices)) _LOGGER.debug("Parsed devices: %s", device_aliases) - platform = base_config[DOMAIN].get(CONF_PLATFORM, SWITCH) + platform = base_config[DOMAIN].get(CONF_PLATFORM, SWITCH_DOMAIN) loop = ( # Create own thread if more than 1 CPU diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index c85478d8f1182f..5df97f79bd2f62 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -296,7 +296,7 @@ def _check_for_constant_alias( # Check for `from homeassistant.components.other import DOMAIN` for name, alias in node.names: - if name == "DOMAIN" and (alias is None or alias == "DOMAIN"): + if name == "DOMAIN" and (alias is None or not alias.endswith("_DOMAIN")): self.add_message( "hass-import-constant-alias", node=node, From 523b52748681d8ad139faf8e0993e14e1b19c942 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:19:35 +0100 Subject: [PATCH 36/36] Use shorthand attributes in omnilogic (#163283) --- homeassistant/components/omnilogic/entity.py | 42 ++++---------------- homeassistant/components/omnilogic/sensor.py | 8 ++-- 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/omnilogic/entity.py b/homeassistant/components/omnilogic/entity.py index 6f7b769fc8fd30..99aac6995897a9 100644 --- a/homeassistant/components/omnilogic/entity.py +++ b/homeassistant/components/omnilogic/entity.py @@ -1,7 +1,5 @@ """Common classes and elements for Omnilogic Integration.""" -from typing import Any - from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -54,40 +52,14 @@ def __init__( unique_id = unique_id.replace(" ", "_") self._kind = kind - self._name = entity_friendly_name - self._unique_id = unique_id + self._attr_name = entity_friendly_name + self._attr_unique_id = unique_id self._item_id = item_id - self._icon = icon - self._attrs: dict[str, Any] = {} - self._msp_system_id = msp_system_id - self._backyard_name = coordinator.data[backyard_id]["BackyardName"] - - @property - def unique_id(self) -> str: - """Return a unique, Home Assistant friendly identifier for this entity.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def icon(self): - """Return the icon for the entity.""" - return self._icon - - @property - def extra_state_attributes(self): - """Return the attributes.""" - return self._attrs - - @property - def device_info(self) -> DeviceInfo: - """Define the device as back yard/MSP System.""" - return DeviceInfo( - identifiers={(DOMAIN, self._msp_system_id)}, + self._attr_icon = icon + self._attr_extra_state_attributes = {} + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, msp_system_id)}, manufacturer="Hayward", model="OmniLogic", - name=self._backyard_name, + name=coordinator.data[backyard_id]["BackyardName"], ) diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index d941eb3ae4df49..522dcc4f3cd3da 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -115,8 +115,10 @@ def native_value(self): hayward_state = None state = None - self._attrs["hayward_temperature"] = hayward_state - self._attrs["hayward_unit_of_measure"] = hayward_unit_of_measure + self._attr_extra_state_attributes["hayward_temperature"] = hayward_state + self._attr_extra_state_attributes["hayward_unit_of_measure"] = ( + hayward_unit_of_measure + ) self._attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT @@ -153,7 +155,7 @@ def native_value(self): ): state = "high" - self._attrs["pump_type"] = pump_type + self._attr_extra_state_attributes["pump_type"] = pump_type return state