Compare commits

..

23 Commits

Author SHA1 Message Date
abmantis
3e4a6d9092 Add refrigerator temperature level select to whirlpool 2026-02-02 23:27:28 +00:00
Brett Adams
c5b9699098 Add model_id and sw_version to Teslemetry device info (#161959)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 21:58:02 +01:00
mettolen
6937bfdf67 Add number entity to Liebherr integration (#162011) 2026-02-02 21:48:39 +01:00
epenet
39ee3fcfaa Move bond service registration (#162075) 2026-02-02 21:47:30 +01:00
J. Diego Rodríguez Royo
16cdfd05a0 Remove coffee machine's hot water sensor's state class at Home Connect (#161246) 2026-02-02 21:30:43 +01:00
mezz64
f49d4787be Bump pyhik to 0.4.2 (#162092)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-02 21:22:42 +01:00
epenet
2076700dc4 Move rainbird service registration (#162089) 2026-02-02 21:02:57 +01:00
Åke Strandberg
76c135913e Update Senz temperature sensor (#162016) 2026-02-02 20:10:46 +01:00
epenet
c3534d5445 Mark tts method type hints as mandatory (#161235) 2026-02-02 19:49:55 +01:00
epenet
fc60b16d65 Mark device_tracker method type hints as mandatory (#161232) 2026-02-02 19:49:26 +01:00
epenet
0443c93f77 Move webostv service registration (#162091) 2026-02-02 20:48:21 +02:00
Bram Kragten
f97cf0e446 Update frontend to 20260128.4 (#162096) 2026-02-02 19:03:38 +01:00
epenet
bd4fa0d5c2 Move reolink service registration (#162085) 2026-02-02 19:02:09 +01:00
Steven Travers
f60d367184 Add learn more data for Analytics in labs (#162094) 2026-02-02 17:22:01 +01:00
epenet
6e231f2ec5 Move husqvarna_automower service registration (#162087) 2026-02-02 17:08:41 +01:00
epenet
13ba2d2e47 Move litterrobot service registration (#162088) 2026-02-02 17:08:07 +01:00
epenet
ba4a163e24 Move roborock service registration (#162090) 2026-02-02 17:07:44 +01:00
epenet
b7db8684db Move elgato service registration (#162086) 2026-02-02 17:05:48 +01:00
Ludovic BOUÉ
a7595dc468 Rename Matter Inovelli VTM31-SN fixture (#162076) 2026-02-02 15:49:11 +01:00
epenet
d2c8c3565b Move blink service registration (#162078) 2026-02-02 14:49:05 +01:00
Ludovic BOUÉ
422d1031f4 Rename Matter Mock air purifier fixture file (#161937) 2026-02-02 14:43:27 +01:00
Viktor Andersson
c9a79cf100 Update Electricity Maps translations (#162074)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-02-02 13:54:07 +01:00
epenet
c42d47a619 Rename service registration function in growatt_server (#162073) 2026-02-02 13:32:33 +01:00
105 changed files with 4655 additions and 3404 deletions

View File

@@ -10,6 +10,7 @@
"preview_features": {
"snapshots": {
"feedback_url": "https://forms.gle/GqvRmgmghSDco8M46",
"learn_more_url": "https://www.home-assistant.io/blog/2026/02/02/about-device-database/",
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new"
}
},

View File

@@ -1,6 +1,7 @@
"""Support for Baidu speech service."""
import logging
from typing import Any
from aip import AipSpeech
import voluptuous as vol
@@ -9,6 +10,7 @@ from homeassistant.components.tts import (
CONF_LANG,
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
Provider,
TtsAudioType,
)
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import config_validation as cv
@@ -85,17 +87,17 @@ class BaiduTTSProvider(Provider):
}
@property
def default_language(self):
def default_language(self) -> str:
"""Return the default language."""
return self._lang
@property
def supported_languages(self):
def supported_languages(self) -> list[str]:
"""Return a list of supported languages."""
return SUPPORTED_LANGUAGES
@property
def default_options(self):
def default_options(self) -> dict[str, Any]:
"""Return a dict including default options."""
return {
CONF_PERSON: self._speech_conf_data[_OPTIONS[CONF_PERSON]],
@@ -105,11 +107,16 @@ class BaiduTTSProvider(Provider):
}
@property
def supported_options(self):
def supported_options(self) -> list[str]:
"""Return a list of supported options."""
return SUPPORTED_OPTIONS
def get_tts_audio(self, message, language, options):
def get_tts_audio(
self,
message: str,
language: str,
options: dict[str, Any],
) -> TtsAudioType:
"""Load TTS from BaiduTTS."""
aip_speech = AipSpeech(

View File

@@ -6,16 +6,9 @@ from typing import Any
from blinkpy.auth import Auth
from blinkpy.blinkpy import Blink
import voluptuous as vol
from homeassistant.components import persistent_notification
from homeassistant.const import (
CONF_FILE_PATH,
CONF_FILENAME,
CONF_NAME,
CONF_PIN,
CONF_SCAN_INTERVAL,
)
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -27,13 +20,6 @@ from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILENAME): cv.string}
)
SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): cv.string})
SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILE_PATH): cv.string}
)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

View File

@@ -9,35 +9,23 @@ from typing import Any
from blinkpy.auth import UnauthorizedError
from blinkpy.camera import BlinkCamera as BlinkCameraAPI
from requests.exceptions import ChunkedEncodingError
import voluptuous as vol
from homeassistant.components.camera import Camera
from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
HomeAssistantError,
ServiceValidationError,
)
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
DEFAULT_BRAND,
DOMAIN,
SERVICE_RECORD,
SERVICE_SAVE_RECENT_CLIPS,
SERVICE_SAVE_VIDEO,
SERVICE_TRIGGER,
)
from .const import DEFAULT_BRAND, DOMAIN
from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
ATTR_VIDEO_CLIP = "video"
ATTR_IMAGE = "image"
PARALLEL_UPDATES = 1
@@ -56,20 +44,6 @@ async def async_setup_entry(
async_add_entities(entities)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(SERVICE_RECORD, None, "record")
platform.async_register_entity_service(SERVICE_TRIGGER, None, "trigger_camera")
platform.async_register_entity_service(
SERVICE_SAVE_RECENT_CLIPS,
{vol.Required(CONF_FILE_PATH): cv.string},
"save_recent_clips",
)
platform.async_register_entity_service(
SERVICE_SAVE_VIDEO,
{vol.Required(CONF_FILENAME): cv.string},
"save_video",
)
class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
"""An implementation of a Blink Camera."""

View File

@@ -20,11 +20,6 @@ TYPE_TEMPERATURE = "temperature"
TYPE_BATTERY = "battery"
TYPE_WIFI_STRENGTH = "wifi_strength"
SERVICE_RECORD = "record"
SERVICE_TRIGGER = "trigger_camera"
SERVICE_SAVE_VIDEO = "save_video"
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
SERVICE_SEND_PIN = "send_pin"
PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,

View File

@@ -4,13 +4,27 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_PIN
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.const import (
ATTR_CONFIG_ENTRY_ID,
CONF_FILE_PATH,
CONF_FILENAME,
CONF_PIN,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers import config_validation as cv, issue_registry as ir, service
from .const import DOMAIN, SERVICE_SEND_PIN
from .const import DOMAIN
SERVICE_RECORD = "record"
SERVICE_TRIGGER = "trigger_camera"
SERVICE_SAVE_VIDEO = "save_video"
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
# Deprecated
SERVICE_SEND_PIN = "send_pin"
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): vol.All(cv.ensure_list, [cv.string]),
@@ -52,3 +66,36 @@ def async_setup_services(hass: HomeAssistant) -> None:
_send_pin,
schema=SERVICE_SEND_PIN_SCHEMA,
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_RECORD,
entity_domain=CAMERA_DOMAIN,
schema=None,
func="record",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_TRIGGER,
entity_domain=CAMERA_DOMAIN,
schema=None,
func="trigger_camera",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SAVE_RECENT_CLIPS,
entity_domain=CAMERA_DOMAIN,
schema={vol.Required(CONF_FILE_PATH): cv.string},
func="save_recent_clips",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SAVE_VIDEO,
entity_domain=CAMERA_DOMAIN,
schema={vol.Required(CONF_FILENAME): cv.string},
func="save_video",
)

View File

@@ -16,14 +16,17 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import SLOW_UPDATE_WARNING
from homeassistant.helpers.typing import ConfigType
from .const import BRIDGE_MAKE, DOMAIN
from .models import BondData
from .services import async_setup_services
from .utils import BondHub
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [
Platform.BUTTON,
Platform.COVER,
@@ -38,6 +41,12 @@ _LOGGER = logging.getLogger(__name__)
type BondConfigEntry = ConfigEntry[BondData]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: BondConfigEntry) -> bool:
"""Set up Bond from a config entry."""
host = entry.data[CONF_HOST]

View File

@@ -5,10 +5,3 @@ BRIDGE_MAKE = "Olibra"
DOMAIN = "bond"
CONF_BOND_ID: str = "bond_id"
SERVICE_SET_FAN_SPEED_TRACKED_STATE = "set_fan_speed_tracked_state"
SERVICE_SET_POWER_TRACKED_STATE = "set_switch_power_tracked_state"
SERVICE_SET_LIGHT_POWER_TRACKED_STATE = "set_light_power_tracked_state"
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE = "set_light_brightness_tracked_state"
ATTR_POWER_STATE = "power_state"

View File

@@ -8,7 +8,6 @@ from typing import Any
from aiohttp.client_exceptions import ClientResponseError
from bond_async import Action, DeviceType, Direction
import voluptuous as vol
from homeassistant.components.fan import (
DIRECTION_FORWARD,
@@ -18,7 +17,6 @@ from homeassistant.components.fan import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.percentage import (
percentage_to_ranged_value,
@@ -27,7 +25,6 @@ from homeassistant.util.percentage import (
from homeassistant.util.scaling import int_states_in_range
from . import BondConfigEntry
from .const import SERVICE_SET_FAN_SPEED_TRACKED_STATE
from .entity import BondEntity
from .models import BondData
from .utils import BondDevice
@@ -44,12 +41,6 @@ async def async_setup_entry(
) -> None:
"""Set up Bond fan devices."""
data = entry.runtime_data
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_SET_FAN_SPEED_TRACKED_STATE,
{vol.Required("speed"): vol.All(vol.Number(scale=0), vol.Range(0, 100))},
"async_set_speed_belief",
)
async_add_entities(
BondFan(data, device)

View File

@@ -7,37 +7,20 @@ from typing import Any
from aiohttp.client_exceptions import ClientResponseError
from bond_async import Action, DeviceType
import voluptuous as vol
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BondConfigEntry
from .const import (
ATTR_POWER_STATE,
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE,
SERVICE_SET_LIGHT_POWER_TRACKED_STATE,
)
from .entity import BondEntity
from .models import BondData
from .utils import BondDevice
_LOGGER = logging.getLogger(__name__)
SERVICE_START_INCREASING_BRIGHTNESS = "start_increasing_brightness"
SERVICE_START_DECREASING_BRIGHTNESS = "start_decreasing_brightness"
SERVICE_STOP = "stop"
ENTITY_SERVICES = [
SERVICE_START_INCREASING_BRIGHTNESS,
SERVICE_START_DECREASING_BRIGHTNESS,
SERVICE_STOP,
]
async def async_setup_entry(
hass: HomeAssistant,
@@ -48,14 +31,6 @@ async def async_setup_entry(
data = entry.runtime_data
hub = data.hub
platform = entity_platform.async_get_current_platform()
for service in ENTITY_SERVICES:
platform.async_register_entity_service(
service,
None,
f"async_{service}",
)
fan_lights: list[Entity] = [
BondLight(data, device)
for device in hub.devices
@@ -94,22 +69,6 @@ async def async_setup_entry(
if DeviceType.is_light(device.type)
]
platform.async_register_entity_service(
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE,
{
vol.Required(ATTR_BRIGHTNESS): vol.All(
vol.Number(scale=0), vol.Range(0, 255)
)
},
"async_set_brightness_belief",
)
platform.async_register_entity_service(
SERVICE_SET_LIGHT_POWER_TRACKED_STATE,
{vol.Required(ATTR_POWER_STATE): vol.All(cv.boolean)},
"async_set_power_belief",
)
async_add_entities(
fan_lights + fan_up_lights + fan_down_lights + fireplaces + fp_lights + lights,
)

View File

@@ -0,0 +1,101 @@
"""Support for Bond services."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service
from .const import DOMAIN
ATTR_POWER_STATE = "power_state"
# Fan
SERVICE_SET_FAN_SPEED_TRACKED_STATE = "set_fan_speed_tracked_state"
# Switch
SERVICE_SET_POWER_TRACKED_STATE = "set_switch_power_tracked_state"
# Light
SERVICE_SET_LIGHT_POWER_TRACKED_STATE = "set_light_power_tracked_state"
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE = "set_light_brightness_tracked_state"
SERVICE_START_INCREASING_BRIGHTNESS = "start_increasing_brightness"
SERVICE_START_DECREASING_BRIGHTNESS = "start_decreasing_brightness"
SERVICE_STOP = "stop"
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Home Assistant services."""
# Fan entity services
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SET_FAN_SPEED_TRACKED_STATE,
entity_domain=FAN_DOMAIN,
schema={vol.Required("speed"): vol.All(vol.Number(scale=0), vol.Range(0, 100))},
func="async_set_speed_belief",
)
# Light entity services
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_START_INCREASING_BRIGHTNESS,
entity_domain=LIGHT_DOMAIN,
schema=None,
func="async_start_increasing_brightness",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_START_DECREASING_BRIGHTNESS,
entity_domain=LIGHT_DOMAIN,
schema=None,
func="async_start_decreasing_brightness",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_STOP,
entity_domain=LIGHT_DOMAIN,
schema=None,
func="async_stop",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE,
entity_domain=LIGHT_DOMAIN,
schema={
vol.Required(ATTR_BRIGHTNESS): vol.All(
vol.Number(scale=0), vol.Range(0, 255)
)
},
func="async_set_brightness_belief",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SET_LIGHT_POWER_TRACKED_STATE,
entity_domain=LIGHT_DOMAIN,
schema={vol.Required(ATTR_POWER_STATE): vol.All(cv.boolean)},
func="async_set_power_belief",
)
# Switch entity services
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SET_POWER_TRACKED_STATE,
entity_domain=SWITCH_DOMAIN,
schema={vol.Required(ATTR_POWER_STATE): cv.boolean},
func="async_set_power_belief",
)

View File

@@ -6,16 +6,13 @@ from typing import Any
from aiohttp.client_exceptions import ClientResponseError
from bond_async import Action, DeviceType
import voluptuous as vol
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BondConfigEntry
from .const import ATTR_POWER_STATE, SERVICE_SET_POWER_TRACKED_STATE
from .entity import BondEntity
@@ -26,12 +23,6 @@ async def async_setup_entry(
) -> None:
"""Set up Bond generic devices."""
data = entry.runtime_data
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_SET_POWER_TRACKED_STATE,
{vol.Required(ATTR_POWER_STATE): cv.boolean},
"async_set_power_belief",
)
async_add_entities(
BondSwitch(data, device)

View File

@@ -21,7 +21,7 @@ async def fetch_latest_carbon_intensity(
em: ElectricityMaps,
config: Mapping[str, Any],
) -> HomeAssistantCarbonIntensityResponse:
"""Fetch the latest carbon intensity based on country code or location coordinates."""
"""Fetch the latest carbon intensity based on zone key or location coordinates."""
request: CoordinatesRequest | ZoneRequest = CoordinatesRequest(
lat=config.get(CONF_LATITUDE, hass.config.latitude),
lon=config.get(CONF_LONGITUDE, hass.config.longitude),

View File

@@ -5,7 +5,7 @@
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"no_data": "No data is available for the location you have selected.",
"no_data": "No data is available for the location or zone you have selected.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
@@ -17,20 +17,20 @@
},
"country": {
"data": {
"country_code": "Country code"
"country_code": "Zone key"
}
},
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::access_token%]"
"api_key": "[%key:common::config_flow::data::api_key%]"
}
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::access_token%]",
"api_key": "[%key:common::config_flow::data::api_key%]",
"location": "[%key:common::config_flow::data::location%]"
},
"description": "Visit the [Electricity Maps page]({register_link}) to request a token."
"description": "Visit the [Electricity Maps app]({register_link}) to request an API key."
}
}
},
@@ -40,7 +40,7 @@
"name": "CO2 intensity",
"state_attributes": {
"country_code": {
"name": "Country code"
"name": "Zone key"
}
}
},
@@ -58,7 +58,7 @@
"location": {
"options": {
"specify_coordinates": "Specify coordinates",
"specify_country_code": "Specify country code",
"specify_country_code": "Specify zone key",
"use_home_location": "Use home location"
}
}

View File

@@ -2,12 +2,23 @@
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .coordinator import ElgatoConfigEntry, ElgatoDataUpdateCoordinator
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [Platform.BUTTON, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ElgatoConfigEntry) -> bool:
"""Set up Elgato Light from a config entry."""
coordinator = ElgatoDataUpdateCoordinator(hass, entry)

View File

@@ -14,6 +14,3 @@ SCAN_INTERVAL = timedelta(seconds=10)
# Attributes
ATTR_ON = "on"
# Services
SERVICE_IDENTIFY = "identify"

View File

@@ -15,13 +15,9 @@ from homeassistant.components.light import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util
from .const import SERVICE_IDENTIFY
from .coordinator import ElgatoConfigEntry, ElgatoDataUpdateCoordinator
from .entity import ElgatoEntity
@@ -37,13 +33,6 @@ async def async_setup_entry(
coordinator = entry.runtime_data
async_add_entities([ElgatoLight(coordinator)])
platform = async_get_current_platform()
platform.async_register_entity_service(
SERVICE_IDENTIFY,
None,
ElgatoLight.async_identify.__name__,
)
class ElgatoLight(ElgatoEntity, LightEntity):
"""Defines an Elgato Light."""

View File

@@ -0,0 +1,25 @@
"""Support for Elgato services."""
from __future__ import annotations
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import service
from .const import DOMAIN
SERVICE_IDENTIFY = "identify"
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_IDENTIFY,
entity_domain=LIGHT_DOMAIN,
schema=None,
func="async_identify",
)

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260128.3"]
"requirements": ["home-assistant-frontend==20260128.4"]
}

View File

@@ -86,7 +86,7 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity):
)
@property
def battery_level(self):
def battery_level(self) -> int | None:
"""Return battery value of the device."""
return self._battery

View File

@@ -27,7 +27,7 @@ from .const import (
)
from .coordinator import GrowattConfigEntry, GrowattCoordinator
from .models import GrowattRuntimeData
from .services import async_register_services
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
@@ -37,7 +37,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Growatt Server component."""
# Register services
await async_register_services(hass)
async_setup_services(hass)
return True

View File

@@ -6,7 +6,7 @@ from datetime import datetime
from typing import TYPE_CHECKING, Any
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import device_registry as dr
@@ -21,7 +21,8 @@ if TYPE_CHECKING:
from .coordinator import GrowattCoordinator
async def async_register_services(hass: HomeAssistant) -> None:
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services for Growatt Server integration."""
def get_min_coordinators() -> dict[str, GrowattCoordinator]:

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["pyhik"],
"quality_scale": "legacy",
"requirements": ["pyHik==0.4.1"]
"requirements": ["pyHik==0.4.2"]
}

View File

@@ -115,7 +115,6 @@ SENSORS = (
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfVolume.MILLILITERS,
device_class=SensorDeviceClass.VOLUME,
state_class=SensorStateClass.TOTAL_INCREASING,
translation_key="hot_water_counter",
),
HomeConnectSensorEntityDescription(

View File

@@ -1,21 +1,25 @@
"""The Husqvarna Automower integration."""
import logging
from aioautomower.session import AutomowerSession
from aiohttp import ClientResponseError
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
config_validation as cv,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from . import api
from .const import DOMAIN
from .coordinator import AutomowerConfigEntry, AutomowerDataUpdateCoordinator
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
@@ -30,6 +34,12 @@ PLATFORMS: list[Platform] = [
]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: AutomowerConfigEntry) -> bool:
"""Set up this integration using UI."""
implementation = (

View File

@@ -142,3 +142,6 @@ ERROR_KEYS = [
"wrong_pin_code",
"zone_generator_problem",
]
MOW = "mow"
PARK = "park"

View File

@@ -1,11 +1,9 @@
"""Husqvarna Automower lawn mower entity."""
from datetime import timedelta
import logging
from typing import TYPE_CHECKING
from aioautomower.model import MowerActivities, MowerStates, WorkArea
import voluptuous as vol
from homeassistant.components.lawn_mower import (
LawnMowerActivity,
@@ -14,16 +12,13 @@ from homeassistant.components.lawn_mower import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AutomowerConfigEntry
from .const import DOMAIN, ERROR_STATES
from .const import DOMAIN, ERROR_STATES, MOW, PARK
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerBaseEntity, handle_sending_exception
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
DOCKED_ACTIVITIES = (MowerActivities.PARKED_IN_CS, MowerActivities.CHARGING)
@@ -41,9 +36,6 @@ SUPPORT_STATE_SERVICES = (
| LawnMowerEntityFeature.PAUSE
| LawnMowerEntityFeature.START_MOWING
)
MOW = "mow"
PARK = "park"
OVERRIDE_MODES = [MOW, PARK]
async def async_setup_entry(
@@ -62,31 +54,6 @@ async def async_setup_entry(
_async_add_new_devices(set(coordinator.data))
coordinator.new_devices_callbacks.append(_async_add_new_devices)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
"override_schedule",
{
vol.Required("override_mode"): vol.In(OVERRIDE_MODES),
vol.Required("duration"): vol.All(
cv.time_period,
cv.positive_timedelta,
vol.Range(min=timedelta(minutes=1), max=timedelta(days=42)),
),
},
"async_override_schedule",
)
platform.async_register_entity_service(
"override_schedule_work_area",
{
vol.Required("work_area_id"): vol.Coerce(int),
vol.Required("duration"): vol.All(
cv.time_period,
cv.positive_timedelta,
vol.Range(min=timedelta(minutes=1), max=timedelta(days=42)),
),
},
"async_override_schedule_work_area",
)
class AutomowerLawnMowerEntity(AutomowerBaseEntity, LawnMowerEntity):

View File

@@ -0,0 +1,49 @@
"""Husqvarna Automower services."""
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.lawn_mower import DOMAIN as LAWN_MOWER_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service
from .const import DOMAIN, MOW, PARK
OVERRIDE_MODES = [MOW, PARK]
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
"override_schedule",
entity_domain=LAWN_MOWER_DOMAIN,
schema={
vol.Required("override_mode"): vol.In(OVERRIDE_MODES),
vol.Required("duration"): vol.All(
cv.time_period,
cv.positive_timedelta,
vol.Range(min=timedelta(minutes=1), max=timedelta(days=42)),
),
},
func="async_override_schedule",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
"override_schedule_work_area",
entity_domain=LAWN_MOWER_DOMAIN,
schema={
vol.Required("work_area_id"): vol.Coerce(int),
vol.Required("duration"): vol.All(
cv.time_period,
cv.positive_timedelta,
vol.Range(min=timedelta(minutes=1), max=timedelta(days=42)),
),
},
func="async_override_schedule_work_area",
)

View File

@@ -17,7 +17,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import LiebherrConfigEntry, LiebherrCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: LiebherrConfigEntry) -> bool:

View File

@@ -0,0 +1,164 @@
"""Number platform for Liebherr integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from pyliebherrhomeapi import (
LiebherrConnectionError,
LiebherrTimeoutError,
TemperatureControl,
TemperatureUnit,
)
from homeassistant.components.number import (
DEFAULT_MAX_VALUE,
DEFAULT_MIN_VALUE,
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import LiebherrConfigEntry, LiebherrCoordinator
from .entity import LiebherrZoneEntity
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class LiebherrNumberEntityDescription(NumberEntityDescription):
"""Describes Liebherr number entity."""
value_fn: Callable[[TemperatureControl], float | None]
min_fn: Callable[[TemperatureControl], float | None]
max_fn: Callable[[TemperatureControl], float | None]
unit_fn: Callable[[TemperatureControl], str]
NUMBER_TYPES: tuple[LiebherrNumberEntityDescription, ...] = (
LiebherrNumberEntityDescription(
key="setpoint_temperature",
translation_key="setpoint_temperature",
device_class=NumberDeviceClass.TEMPERATURE,
native_step=1,
value_fn=lambda control: control.target,
min_fn=lambda control: control.min,
max_fn=lambda control: control.max,
unit_fn=lambda control: (
UnitOfTemperature.FAHRENHEIT
if control.unit == TemperatureUnit.FAHRENHEIT
else UnitOfTemperature.CELSIUS
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: LiebherrConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Liebherr number entities."""
coordinators = entry.runtime_data
async_add_entities(
LiebherrNumber(
coordinator=coordinator,
zone_id=temp_control.zone_id,
description=description,
)
for coordinator in coordinators.values()
for temp_control in coordinator.data.get_temperature_controls().values()
for description in NUMBER_TYPES
)
class LiebherrNumber(LiebherrZoneEntity, NumberEntity):
"""Representation of a Liebherr number entity."""
entity_description: LiebherrNumberEntityDescription
def __init__(
self,
coordinator: LiebherrCoordinator,
zone_id: int,
description: LiebherrNumberEntityDescription,
) -> None:
"""Initialize the number entity."""
super().__init__(coordinator, zone_id)
self.entity_description = description
self._attr_unique_id = f"{coordinator.device_id}_{description.key}_{zone_id}"
# If device has only one zone, use translation key without zone suffix
temp_controls = coordinator.data.get_temperature_controls()
if len(temp_controls) > 1 and (zone_key := self._get_zone_translation_key()):
self._attr_translation_key = f"{description.translation_key}_{zone_key}"
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
if (temp_control := self.temperature_control) is None:
return None
return self.entity_description.unit_fn(temp_control)
@property
def native_value(self) -> float | None:
"""Return the current value."""
# temperature_control is guaranteed to exist when entity is available
return self.entity_description.value_fn(
self.temperature_control # type: ignore[arg-type]
)
@property
def native_min_value(self) -> float:
"""Return the minimum value."""
if (temp_control := self.temperature_control) is None:
return DEFAULT_MIN_VALUE
if (min_val := self.entity_description.min_fn(temp_control)) is None:
return DEFAULT_MIN_VALUE
return min_val
@property
def native_max_value(self) -> float:
"""Return the maximum value."""
if (temp_control := self.temperature_control) is None:
return DEFAULT_MAX_VALUE
if (max_val := self.entity_description.max_fn(temp_control)) is None:
return DEFAULT_MAX_VALUE
return max_val
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.temperature_control is not None
async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
# temperature_control is guaranteed to exist when entity is available
temp_control = self.temperature_control
unit = (
TemperatureUnit.FAHRENHEIT
if temp_control.unit == TemperatureUnit.FAHRENHEIT # type: ignore[union-attr]
else TemperatureUnit.CELSIUS
)
try:
await self.coordinator.client.set_temperature(
device_id=self.coordinator.device_id,
zone_id=self._zone_id,
target=int(value),
unit=unit,
)
except (LiebherrConnectionError, LiebherrTimeoutError) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_temperature_failed",
) from err
await self.coordinator.async_request_refresh()

View File

@@ -55,23 +55,16 @@ async def async_setup_entry(
) -> None:
"""Set up Liebherr sensor entities."""
coordinators = entry.runtime_data
entities: list[LiebherrSensor] = []
for coordinator in coordinators.values():
# Get all temperature controls for this device
temp_controls = coordinator.data.get_temperature_controls()
for temp_control in temp_controls.values():
entities.extend(
LiebherrSensor(
coordinator=coordinator,
zone_id=temp_control.zone_id,
description=description,
)
for description in SENSOR_TYPES
)
async_add_entities(entities)
async_add_entities(
LiebherrSensor(
coordinator=coordinator,
zone_id=temp_control.zone_id,
description=description,
)
for coordinator in coordinators.values()
for temp_control in coordinator.data.get_temperature_controls().values()
for description in SENSOR_TYPES
)
class LiebherrSensor(LiebherrZoneEntity, SensorEntity):
@@ -108,9 +101,9 @@ class LiebherrSensor(LiebherrZoneEntity, SensorEntity):
@property
def native_value(self) -> StateType:
"""Return the current value."""
if (temp_control := self.temperature_control) is None:
return None
return self.entity_description.value_fn(temp_control)
# temperature_control is guaranteed to exist when entity is available
assert self.temperature_control is not None
return self.entity_description.value_fn(self.temperature_control)
@property
def available(self) -> bool:

View File

@@ -33,6 +33,20 @@
}
},
"entity": {
"number": {
"setpoint_temperature": {
"name": "Setpoint"
},
"setpoint_temperature_bottom_zone": {
"name": "Bottom zone setpoint"
},
"setpoint_temperature_middle_zone": {
"name": "Middle zone setpoint"
},
"setpoint_temperature_top_zone": {
"name": "Top zone setpoint"
}
},
"sensor": {
"bottom_zone": {
"name": "Bottom zone"
@@ -44,5 +58,10 @@
"name": "Top zone"
}
}
},
"exceptions": {
"set_temperature_failed": {
"message": "Failed to set temperature"
}
}
}

View File

@@ -6,11 +6,15 @@ import itertools
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .coordinator import LitterRobotConfigEntry, LitterRobotDataUpdateCoordinator
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
@@ -23,6 +27,12 @@ PLATFORMS = [
]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: LitterRobotConfigEntry) -> bool:
"""Set up Litter-Robot from a config entry."""
coordinator = LitterRobotDataUpdateCoordinator(hass, entry)

View File

@@ -0,0 +1,30 @@
"""Litter-Robot services."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service
from .const import DOMAIN
SERVICE_SET_SLEEP_MODE = "set_sleep_mode"
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SET_SLEEP_MODE,
entity_domain=VACUUM_DOMAIN,
schema={
vol.Required("enabled"): cv.boolean,
vol.Optional("start_time"): cv.time,
},
func="async_set_sleep_mode",
)

View File

@@ -7,7 +7,6 @@ from typing import Any
from pylitterbot import LitterRobot
from pylitterbot.enums import LitterBoxStatus
import voluptuous as vol
from homeassistant.components.vacuum import (
StateVacuumEntity,
@@ -16,15 +15,12 @@ from homeassistant.components.vacuum import (
VacuumEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .coordinator import LitterRobotConfigEntry
from .entity import LitterRobotEntity
SERVICE_SET_SLEEP_MODE = "set_sleep_mode"
LITTER_BOX_STATUS_STATE_MAP = {
LitterBoxStatus.CLEAN_CYCLE: VacuumActivity.CLEANING,
LitterBoxStatus.EMPTY_CYCLE: VacuumActivity.CLEANING,
@@ -57,16 +53,6 @@ async def async_setup_entry(
for robot in coordinator.litter_robots()
)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_SET_SLEEP_MODE,
{
vol.Required("enabled"): cv.boolean,
vol.Optional("start_time"): cv.time,
},
"async_set_sleep_mode",
)
class LitterRobotCleaner(LitterRobotEntity[LitterRobot], StateVacuumEntity):
"""Litter-Robot "Vacuum" Cleaner."""

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from typing import Any
from speak2mary import MaryTTS
import voluptuous as vol
@@ -9,6 +11,7 @@ from homeassistant.components.tts import (
CONF_LANG,
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
Provider,
TtsAudioType,
)
from homeassistant.const import CONF_EFFECT, CONF_HOST, CONF_PORT
from homeassistant.helpers import config_validation as cv
@@ -66,26 +69,28 @@ class MaryTTSProvider(Provider):
self.name = "MaryTTS"
@property
def default_language(self):
def default_language(self) -> str:
"""Return the default language."""
return self._mary.locale
@property
def supported_languages(self):
def supported_languages(self) -> list[str]:
"""Return list of supported languages."""
return SUPPORT_LANGUAGES
@property
def default_options(self):
def default_options(self) -> dict[str, Any]:
"""Return dict include default options."""
return {CONF_EFFECT: self._effects}
@property
def supported_options(self):
def supported_options(self) -> list[str]:
"""Return a list of supported options."""
return SUPPORT_OPTIONS
def get_tts_audio(self, message, language, options):
def get_tts_audio(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Load TTS from MaryTTS."""
effects = options[CONF_EFFECT]

View File

@@ -1014,7 +1014,6 @@ DISCOVERY_SCHEMAS = [
device_to_ha=lambda x: x.energy,
),
entity_class=MatterSensor,
allow_none_value=True,
required_attributes=(
clusters.ElectricalEnergyMeasurement.Attributes.CumulativeEnergyImported,
),

View File

@@ -1,6 +1,7 @@
"""Support for the Microsoft Cognitive Services text-to-speech service."""
import logging
from typing import Any
from pycsspeechtts import pycsspeechtts
from requests.exceptions import HTTPError
@@ -10,6 +11,7 @@ from homeassistant.components.tts import (
CONF_LANG,
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
Provider,
TtsAudioType,
)
from homeassistant.const import CONF_API_KEY, CONF_REGION, CONF_TYPE, PERCENTAGE
from homeassistant.generated.microsoft_tts import SUPPORTED_LANGUAGES
@@ -89,26 +91,28 @@ class MicrosoftProvider(Provider):
self.name = "Microsoft"
@property
def default_language(self):
def default_language(self) -> str:
"""Return the default language."""
return self._lang
@property
def supported_languages(self):
def supported_languages(self) -> list[str]:
"""Return list of supported languages."""
return SUPPORTED_LANGUAGES
return list(SUPPORTED_LANGUAGES)
@property
def supported_options(self):
def supported_options(self) -> list[str]:
"""Return list of supported options like voice, emotion."""
return [CONF_GENDER, CONF_TYPE]
@property
def default_options(self):
def default_options(self) -> dict[str, Any]:
"""Return a dict include default options."""
return {CONF_GENDER: self._gender, CONF_TYPE: self._type}
def get_tts_audio(self, message, language, options):
def get_tts_audio(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Load TTS from Microsoft."""
if language is None:
language = self._lang

View File

@@ -5,6 +5,7 @@ import os
import shutil
import subprocess
import tempfile
from typing import Any
import voluptuous as vol
@@ -12,6 +13,7 @@ from homeassistant.components.tts import (
CONF_LANG,
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
Provider,
TtsAudioType,
)
_LOGGER = logging.getLogger(__name__)
@@ -42,16 +44,18 @@ class PicoProvider(Provider):
self.name = "PicoTTS"
@property
def default_language(self):
def default_language(self) -> str:
"""Return the default language."""
return self._lang
@property
def supported_languages(self):
def supported_languages(self) -> list[str]:
"""Return list of supported languages."""
return SUPPORT_LANGUAGES
def get_tts_audio(self, message, language, options):
def get_tts_audio(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Load TTS using pico2wave."""
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpf:
fname = tmpf.name

View File

@@ -18,19 +18,26 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.typing import ConfigType
from .const import CONF_SERIAL_NUMBER
from .const import CONF_SERIAL_NUMBER, DOMAIN
from .coordinator import (
RainbirdScheduleUpdateCoordinator,
RainbirdUpdateCoordinator,
async_create_clientsession,
)
from .services import async_setup_services
from .types import RainbirdConfigEntry, RainbirdData
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.CALENDAR,
@@ -40,9 +47,6 @@ PLATFORMS = [
]
DOMAIN = "rainbird"
def _async_register_clientsession_shutdown(
hass: HomeAssistant,
entry: RainbirdConfigEntry,
@@ -61,6 +65,12 @@ def _async_register_clientsession_shutdown(
entry.async_on_unload(_async_close_websession)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: RainbirdConfigEntry) -> bool:
"""Set up the config entry for Rain Bird."""

View File

@@ -0,0 +1,32 @@
"""Rain Bird Irrigation system services."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service
from homeassistant.helpers.typing import VolDictType
from .const import ATTR_DURATION, DOMAIN
SERVICE_START_IRRIGATION = "start_irrigation"
SERVICE_SCHEMA_IRRIGATION: VolDictType = {
vol.Required(ATTR_DURATION): cv.positive_float,
}
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_START_IRRIGATION,
entity_domain=SWITCH_DOMAIN,
schema=SERVICE_SCHEMA_IRRIGATION,
func="async_turn_on",
)

View File

@@ -6,15 +6,12 @@ import logging
from typing import Any
from pyrainbird.exceptions import RainbirdApiException, RainbirdDeviceBusyException
import voluptuous as vol
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import VolDictType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTR_DURATION, CONF_IMPORTED_NAMES, DOMAIN, MANUFACTURER
@@ -23,12 +20,6 @@ from .types import RainbirdConfigEntry
_LOGGER = logging.getLogger(__name__)
SERVICE_START_IRRIGATION = "start_irrigation"
SERVICE_SCHEMA_IRRIGATION: VolDictType = {
vol.Required(ATTR_DURATION): cv.positive_float,
}
async def async_setup_entry(
hass: HomeAssistant,
@@ -47,13 +38,6 @@ async def async_setup_entry(
for zone in coordinator.data.zones
)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_START_IRRIGATION,
SERVICE_SCHEMA_IRRIGATION,
"async_turn_on",
)
class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity):
"""Representation of a Rain Bird switch."""

View File

@@ -7,22 +7,17 @@ from dataclasses import dataclass
from typing import Any
from reolink_aio.api import GuardEnum, Host, PtzEnum
import voluptuous as vol
from homeassistant.components.button import (
ButtonDeviceClass,
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.components.camera import CameraEntityFeature
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import SUPPORT_PTZ_SPEED
from .entity import (
ReolinkChannelCoordinatorEntity,
ReolinkChannelEntityDescription,
@@ -32,9 +27,6 @@ from .entity import (
from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error
PARALLEL_UPDATES = 0
ATTR_SPEED = "speed"
SUPPORT_PTZ_SPEED = CameraEntityFeature.STREAM
SERVICE_PTZ_MOVE = "ptz_move"
@dataclass(frozen=True, kw_only=True)
@@ -178,14 +170,6 @@ async def async_setup_entry(
)
async_add_entities(entities)
platform = async_get_current_platform()
platform.async_register_entity_service(
SERVICE_PTZ_MOVE,
{vol.Required(ATTR_SPEED): cv.positive_int},
"async_ptz_move",
[SUPPORT_PTZ_SPEED],
)
class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity):
"""Base button entity class for Reolink IP cameras."""
@@ -219,9 +203,8 @@ class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity):
await self.entity_description.method(self._host.api, self._channel)
@raise_translated_error
async def async_ptz_move(self, **kwargs: Any) -> None:
async def async_ptz_move(self, *, speed: int) -> None:
"""PTZ move with speed."""
speed = kwargs[ATTR_SPEED]
await self._host.api.set_ptz_command(
self._channel, command=self.entity_description.ptz_cmd, speed=speed
)

View File

@@ -1,5 +1,7 @@
"""Constants for the Reolink Camera integration."""
from homeassistant.components.camera import CameraEntityFeature
DOMAIN = "reolink"
CONF_USE_HTTPS = "use_https"
@@ -13,3 +15,5 @@ CONF_FIRMWARE_CHECK_TIME = "firmware_check_time"
BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL = 3600 # seconds
BATTERY_WAKE_UPDATE_INTERVAL = 6 * BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL
BATTERY_ALL_WAKE_UPDATE_INTERVAL = 2 * BATTERY_WAKE_UPDATE_INTERVAL
SUPPORT_PTZ_SPEED = CameraEntityFeature.STREAM

View File

@@ -6,17 +6,24 @@ from reolink_aio.api import Chime
from reolink_aio.enums import ChimeToneEnum
import voluptuous as vol
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
service,
)
from .const import DOMAIN
from .const import DOMAIN, SUPPORT_PTZ_SPEED
from .host import ReolinkHost
from .util import get_device_uid_and_ch, raise_translated_error
ATTR_RINGTONE = "ringtone"
ATTR_SPEED = "speed"
SERVICE_PTZ_MOVE = "ptz_move"
@raise_translated_error
@@ -76,3 +83,12 @@ def async_setup_services(hass: HomeAssistant) -> None:
}
),
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_PTZ_MOVE,
entity_domain=BUTTON_DOMAIN,
schema={vol.Required(ATTR_SPEED): cv.positive_int},
func="async_ptz_move",
required_features=[SUPPORT_PTZ_SPEED],
)

View File

@@ -23,8 +23,9 @@ from roborock.mqtt.session import MqttSessionUnauthorized
from homeassistant.const import CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_BASE_URL,
@@ -47,12 +48,20 @@ from .coordinator import (
RoborockWetDryVacUpdateCoordinator,
)
from .roborock_storage import CacheStore, async_cleanup_map_storage
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> bool:
"""Set up roborock from a config entry."""
await async_cleanup_map_storage(hass, entry.entry_id)

View File

@@ -52,12 +52,10 @@ IMAGE_CACHE_INTERVAL = timedelta(seconds=30)
MAP_SLEEP = 3
GET_MAPS_SERVICE_NAME = "get_maps"
MAP_SCALE = 4
MAP_FILE_FORMAT = "PNG"
MAP_FILENAME_SUFFIX = ".png"
SET_VACUUM_GOTO_POSITION_SERVICE_NAME = "set_vacuum_goto_position"
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME = "get_vacuum_current_position"
A01_UPDATE_INTERVAL = timedelta(minutes=1)

View File

@@ -0,0 +1,53 @@
"""Roborock services."""
import voluptuous as vol
from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN
from homeassistant.core import HomeAssistant, SupportsResponse, callback
from homeassistant.helpers import config_validation as cv, service
from .const import DOMAIN
GET_MAPS_SERVICE_NAME = "get_maps"
SET_VACUUM_GOTO_POSITION_SERVICE_NAME = "set_vacuum_goto_position"
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME = "get_vacuum_current_position"
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
GET_MAPS_SERVICE_NAME,
entity_domain=VACUUM_DOMAIN,
schema=None,
func="get_maps",
supports_response=SupportsResponse.ONLY,
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
entity_domain=VACUUM_DOMAIN,
schema=None,
func="get_vacuum_current_position",
supports_response=SupportsResponse.ONLY,
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
entity_domain=VACUUM_DOMAIN,
schema=cv.make_entity_service_schema(
{
vol.Required("x"): vol.Coerce(int),
vol.Required("y"): vol.Coerce(int),
},
),
func="async_set_vacuum_goto_position",
supports_response=SupportsResponse.NONE,
)

View File

@@ -6,24 +6,17 @@ from typing import Any
from roborock.data import RoborockStateCode, SCWindMapping, WorkStatusMapping
from roborock.exceptions import RoborockException
from roborock.roborock_typing import RoborockCommand
import voluptuous as vol
from homeassistant.components.vacuum import (
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
from homeassistant.core import HomeAssistant, ServiceResponse
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
DOMAIN,
GET_MAPS_SERVICE_NAME,
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
)
from .const import DOMAIN
from .coordinator import (
RoborockB01Q7UpdateCoordinator,
RoborockConfigEntry,
@@ -92,33 +85,6 @@ async def async_setup_entry(
for coordinator in config_entry.runtime_data.b01
if isinstance(coordinator, RoborockB01Q7UpdateCoordinator)
)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
GET_MAPS_SERVICE_NAME,
None,
RoborockVacuum.get_maps.__name__,
supports_response=SupportsResponse.ONLY,
)
platform.async_register_entity_service(
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
None,
RoborockVacuum.get_vacuum_current_position.__name__,
supports_response=SupportsResponse.ONLY,
)
platform.async_register_entity_service(
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
cv.make_entity_service_schema(
{
vol.Required("x"): vol.Coerce(int),
vol.Required("y"): vol.Coerce(int),
},
),
RoborockVacuum.async_set_vacuum_goto_position.__name__,
supports_response=SupportsResponse.NONE,
)
class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):

View File

@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -81,6 +81,12 @@ class SENZSensor(CoordinatorEntity[SENZDataUpdateCoordinator], SensorEntity):
serial_number=thermostat.serial_number,
)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._thermostat = self.coordinator.data[self._thermostat.serial_number]
self.async_write_ha_state()
@property
def available(self) -> bool:
"""Return True if the thermostat is available."""

View File

@@ -41,7 +41,7 @@ from .coordinator import (
TeslemetryEnergySiteLiveCoordinator,
TeslemetryVehicleDataCoordinator,
)
from .helpers import flatten
from .helpers import async_update_device_sw_version, flatten
from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData
from .services import async_setup_services
@@ -161,13 +161,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
coordinator = TeslemetryVehicleDataCoordinator(
hass, entry, vehicle, product
)
firmware = vehicle_metadata[vin].get("firmware")
device = DeviceInfo(
identifiers={(DOMAIN, vin)},
manufacturer="Tesla",
configuration_url="https://teslemetry.com/console",
name=product["display_name"],
model=vehicle.model,
model_id=vin[3],
serial_number=vin,
sw_version=firmware,
)
current_devices.add((DOMAIN, vin))
@@ -185,7 +188,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
create_handle_vehicle_stream(vin, coordinator),
{"vin": vin},
)
firmware = vehicle_metadata[vin].get("firmware", "Unknown")
stream_vehicle = stream.get_vehicle(vin)
poll = vehicle_metadata[vin].get("polling", False)
@@ -276,23 +278,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
),
)
# Add energy device models
for energysite in energysites:
models = set()
for gateway in energysite.info_coordinator.data.get("components_gateways", []):
if gateway.get("part_name"):
models.add(gateway["part_name"])
for battery in energysite.info_coordinator.data.get("components_batteries", []):
if battery.get("part_name"):
models.add(battery["part_name"])
if models:
energysite.device["model"] = ", ".join(sorted(models))
# Register listeners for polling vehicle sw_version updates
for vehicle_data in vehicles:
if vehicle_data.poll:
entry.async_on_unload(
vehicle_data.coordinator.async_add_listener(
create_vehicle_polling_listener(
hass, vehicle_data.vin, vehicle_data.coordinator
)
)
)
# Create the energy site device regardless of it having entities
# This is so users with a Wall Connector but without a Powerwall can still make service calls
device_registry.async_get_or_create(
config_entry_id=entry.entry_id, **energysite.device
)
# Setup energy devices with models, versions, and listeners
for energysite in energysites:
async_setup_energy_device(hass, entry, energysite, device_registry)
# Remove devices that are no longer present
for device_entry in dr.async_entries_for_config_entry(
@@ -369,6 +368,40 @@ def create_handle_vehicle_stream(vin: str, coordinator) -> Callable[[dict], None
return handle_vehicle_stream
def async_setup_energy_device(
hass: HomeAssistant,
entry: TeslemetryConfigEntry,
energysite: TeslemetryEnergyData,
device_registry: dr.DeviceRegistry,
) -> None:
"""Set up energy device with models, versions, and listeners."""
data = energysite.info_coordinator.data
models = set()
for component in (
*data.get("components_gateways", []),
*data.get("components_batteries", []),
):
if part_name := component.get("part_name"):
models.add(part_name)
if models:
energysite.device["model"] = ", ".join(sorted(models))
if version := data.get("version"):
energysite.device["sw_version"] = version
device_registry.async_get_or_create(
config_entry_id=entry.entry_id, **energysite.device
)
entry.async_on_unload(
energysite.info_coordinator.async_add_listener(
create_energy_info_listener(
hass, energysite.id, energysite.info_coordinator
)
)
)
async def async_setup_stream(
hass: HomeAssistant, entry: TeslemetryConfigEntry, vehicle: TeslemetryVehicleData
):
@@ -380,3 +413,54 @@ async def async_setup_stream(
vehicle.stream_vehicle.prefer_typed(True),
f"Prefer typed for {vehicle.vin}",
)
entry.async_on_unload(
vehicle.stream_vehicle.listen_Version(
create_vehicle_streaming_listener(hass, vehicle.vin)
)
)
def create_vehicle_streaming_listener(
hass: HomeAssistant, vin: str
) -> Callable[[str | None], None]:
"""Create a listener for vehicle streaming version updates."""
def handle_version(value: str | None) -> None:
"""Handle version update from stream."""
if value is not None:
# Remove build from version (e.g., "2024.44.25 abc123" -> "2024.44.25")
sw_version = value.split(" ")[0]
async_update_device_sw_version(hass, vin, sw_version)
return handle_version
def create_vehicle_polling_listener(
hass: HomeAssistant, vin: str, coordinator: TeslemetryVehicleDataCoordinator
) -> Callable[[], None]:
"""Create a listener for vehicle polling coordinator updates."""
def handle_update() -> None:
"""Handle coordinator update."""
if version := coordinator.data.get("vehicle_state_car_version"):
# Remove build from version (e.g., "2024.44.25 abc123" -> "2024.44.25")
sw_version = version.split(" ")[0]
async_update_device_sw_version(hass, vin, sw_version)
return handle_update
def create_energy_info_listener(
hass: HomeAssistant,
site_id: int,
coordinator: TeslemetryEnergySiteInfoCoordinator,
) -> Callable[[], None]:
"""Create a listener for energy site info coordinator updates."""
def handle_update() -> None:
"""Handle coordinator update."""
if version := coordinator.data.get("version"):
async_update_device_sw_version(hass, str(site_id), version)
return handle_update

View File

@@ -4,7 +4,9 @@ from typing import Any
from tesla_fleet_api.exceptions import TeslaFleetError
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from .const import DOMAIN, LOGGER
@@ -68,3 +70,14 @@ async def handle_vehicle_command(command) -> Any:
)
# Response with result of true
return result
@callback
def async_update_device_sw_version(
hass: HomeAssistant, identifier: str, sw_version: str
) -> None:
"""Update the software version in the device registry."""
dev_reg = dr.async_get(hass)
if device := dev_reg.async_get_device(identifiers={(DOMAIN, identifier)}):
if device.sw_version != sw_version:
dev_reg.async_update_device(device.id, sw_version=sw_version)

View File

@@ -46,11 +46,7 @@ rules:
test fixture. Clarify _alt and _noscope fixture purposes. Test error messages in
test_service_validation_errors.
# Gold
devices:
status: todo
comment: |
Add model id to device info. VIN sensor may be redundant (already serial number in device).
Version sensor should be sw_version in device info instead.
devices: done
diagnostics: done
discovery:
status: exempt

View File

@@ -1532,10 +1532,6 @@ ENERGY_INFO_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
),
SensorEntityDescription(
key="version",
entity_category=EntityCategory.DIAGNOSTIC,
),
)
ENERGY_HISTORY_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = tuple(

View File

@@ -1032,9 +1032,6 @@
"vehicle_state_tpms_pressure_rr": {
"name": "Tire pressure rear right"
},
"version": {
"name": "Version"
},
"vin": {
"name": "[%key:component::teslemetry::common::vehicle%]",
"state": {

View File

@@ -132,7 +132,7 @@ class TraccarEntity(TrackerEntity, RestoreEntity):
)
@property
def battery_level(self):
def battery_level(self) -> int | None:
"""Return battery value of the device."""
return self._battery

View File

@@ -3,6 +3,7 @@
import asyncio
from http import HTTPStatus
import logging
from typing import TYPE_CHECKING, Any
import aiohttp
import voluptuous as vol
@@ -11,6 +12,7 @@ from homeassistant.components.tts import (
CONF_LANG,
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
Provider,
TtsAudioType,
)
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import config_validation as cv
@@ -182,17 +184,21 @@ class VoiceRSSProvider(Provider):
}
@property
def default_language(self):
def default_language(self) -> str:
"""Return the default language."""
return self._lang
@property
def supported_languages(self):
def supported_languages(self) -> list[str]:
"""Return list of supported languages."""
return SUPPORT_LANGUAGES
async def async_get_tts_audio(self, message, language, options):
async def async_get_tts_audio(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Load TTS from VoiceRSS."""
if TYPE_CHECKING:
assert self.hass
websession = async_get_clientsession(self.hass)
form_data = self._form_data.copy()

View File

@@ -1,6 +1,7 @@
"""Support for IBM Watson TTS integration."""
import logging
from typing import Any
from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
from ibm_watson import TextToSpeechV1
@@ -9,6 +10,7 @@ import voluptuous as vol
from homeassistant.components.tts import (
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
Provider,
TtsAudioType,
)
from homeassistant.helpers import config_validation as cv
@@ -163,26 +165,28 @@ class WatsonTTSProvider(Provider):
self.name = "Watson TTS"
@property
def supported_languages(self):
def supported_languages(self) -> list[str]:
"""Return a list of supported languages."""
return self.supported_langs
@property
def default_language(self):
def default_language(self) -> str:
"""Return the default language."""
return self.default_lang
@property
def default_options(self):
def default_options(self) -> dict[str, Any]:
"""Return dict include default options."""
return {CONF_VOICE: self.default_voice}
@property
def supported_options(self):
def supported_options(self) -> list[str]:
"""Return a list of supported options."""
return [CONF_VOICE]
def get_tts_audio(self, message, language, options):
def get_tts_audio(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Request TTS file from Watson TTS."""
response = self.service.synthesize(
text=message, accept=self.output_format, voice=options[CONF_VOICE]

View File

@@ -23,6 +23,7 @@ from homeassistant.helpers.typing import ConfigType
from .const import DATA_HASS_CONFIG, DOMAIN, PLATFORMS, WEBOSTV_EXCEPTIONS
from .helpers import WebOsTvConfigEntry, update_client_key
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -31,6 +32,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LG webOS TV platform."""
hass.data.setdefault(DOMAIN, {DATA_HASS_CONFIG: config})
async_setup_services(hass)
return True

View File

@@ -12,17 +12,12 @@ PLATFORMS = [Platform.MEDIA_PLAYER]
DATA_HASS_CONFIG = "hass_config"
DEFAULT_NAME = "LG webOS TV"
ATTR_BUTTON = "button"
ATTR_PAYLOAD = "payload"
ATTR_SOUND_OUTPUT = "sound_output"
CONF_ON_ACTION = "turn_on_action"
CONF_SOURCES = "sources"
SERVICE_BUTTON = "button"
SERVICE_COMMAND = "command"
SERVICE_SELECT_SOUND_OUTPUT = "select_sound_output"
LIVE_TV_APP_ID = "com.webos.app.livetv"
WEBOSTV_EXCEPTIONS = (

View File

@@ -12,7 +12,6 @@ import logging
from typing import Any, Concatenate, cast
from aiowebostv import WebOsTvPairError, WebOsTvState
import voluptuous as vol
from homeassistant import util
from homeassistant.components.media_player import (
@@ -22,27 +21,21 @@ from homeassistant.components.media_player import (
MediaPlayerState,
MediaType,
)
from homeassistant.const import ATTR_COMMAND, ATTR_SUPPORTED_FEATURES
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
from homeassistant.const import ATTR_SUPPORTED_FEATURES
from homeassistant.core import HomeAssistant, ServiceResponse
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.trigger import PluggableAction
from homeassistant.helpers.typing import VolDictType
from .const import (
ATTR_BUTTON,
ATTR_PAYLOAD,
ATTR_SOUND_OUTPUT,
CONF_SOURCES,
DOMAIN,
LIVE_TV_APP_ID,
SERVICE_BUTTON,
SERVICE_COMMAND,
SERVICE_SELECT_SOUND_OUTPUT,
WEBOSTV_EXCEPTIONS,
)
from .helpers import WebOsTvConfigEntry, update_client_key
@@ -70,34 +63,6 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=10)
BUTTON_SCHEMA: VolDictType = {vol.Required(ATTR_BUTTON): cv.string}
COMMAND_SCHEMA: VolDictType = {
vol.Required(ATTR_COMMAND): cv.string,
vol.Optional(ATTR_PAYLOAD): dict,
}
SOUND_OUTPUT_SCHEMA: VolDictType = {vol.Required(ATTR_SOUND_OUTPUT): cv.string}
SERVICES = (
(
SERVICE_BUTTON,
BUTTON_SCHEMA,
"async_button",
SupportsResponse.NONE,
),
(
SERVICE_COMMAND,
COMMAND_SCHEMA,
"async_command",
SupportsResponse.OPTIONAL,
),
(
SERVICE_SELECT_SOUND_OUTPUT,
SOUND_OUTPUT_SCHEMA,
"async_select_sound_output",
SupportsResponse.OPTIONAL,
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -105,12 +70,6 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the LG webOS TV platform."""
platform = entity_platform.async_get_current_platform()
for service_name, schema, method, supports_response in SERVICES:
platform.async_register_entity_service(
service_name, schema, method, supports_response=supports_response
)
async_add_entities([LgWebOSMediaPlayerEntity(entry)])

View File

@@ -0,0 +1,63 @@
"""LG webOS TV services."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.const import ATTR_COMMAND
from homeassistant.core import HomeAssistant, SupportsResponse, callback
from homeassistant.helpers import config_validation as cv, service
from homeassistant.helpers.typing import VolDictType
from .const import ATTR_PAYLOAD, ATTR_SOUND_OUTPUT, DOMAIN
ATTR_BUTTON = "button"
SERVICE_BUTTON = "button"
SERVICE_COMMAND = "command"
SERVICE_SELECT_SOUND_OUTPUT = "select_sound_output"
BUTTON_SCHEMA: VolDictType = {vol.Required(ATTR_BUTTON): cv.string}
COMMAND_SCHEMA: VolDictType = {
vol.Required(ATTR_COMMAND): cv.string,
vol.Optional(ATTR_PAYLOAD): dict,
}
SOUND_OUTPUT_SCHEMA: VolDictType = {vol.Required(ATTR_SOUND_OUTPUT): cv.string}
SERVICES = (
(
SERVICE_BUTTON,
BUTTON_SCHEMA,
"async_button",
SupportsResponse.NONE,
),
(
SERVICE_COMMAND,
COMMAND_SCHEMA,
"async_command",
SupportsResponse.OPTIONAL,
),
(
SERVICE_SELECT_SOUND_OUTPUT,
SOUND_OUTPUT_SCHEMA,
"async_select_sound_output",
SupportsResponse.OPTIONAL,
),
)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services."""
for service_name, schema, method, supports_response in SERVICES:
service.async_register_platform_entity_service(
hass,
DOMAIN,
service_name,
entity_domain=MEDIA_PLAYER_DOMAIN,
schema=schema,
func=method,
supports_response=supports_response,
)

View File

@@ -17,7 +17,7 @@ from .const import BRANDS_CONF_MAP, CONF_BRAND, DOMAIN, REGIONS_CONF_MAP
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SELECT, Platform.SENSOR]
type WhirlpoolConfigEntry = ConfigEntry[AppliancesManager]

View File

@@ -0,0 +1,91 @@
"""The select platform for Whirlpool Appliances."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import logging
from typing import Final, override
from whirlpool.appliance import Appliance
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import WhirlpoolConfigEntry
from .const import DOMAIN
from .entity import WhirlpoolEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class WhirlpoolSelectDescription(SelectEntityDescription):
"""Class describing Whirlpool select entities."""
value_fn: Callable[[Appliance], str | None]
set_fn: Callable[[Appliance, str], Awaitable[bool]]
REFRIGERATOR_DESCRIPTIONS: Final[tuple[WhirlpoolSelectDescription, ...]] = (
WhirlpoolSelectDescription(
key="refrigerator_temperature",
translation_key="refrigerator_temperature",
options=["-4", "-2", "0", "3", "5"],
value_fn=lambda fridge: str(val)
if (val := fridge.get_offset_temp()) is not None
else None,
set_fn=lambda fridge, option: fridge.set_offset_temp(int(option)),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: WhirlpoolConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the select platform."""
appliances_manager = config_entry.runtime_data
async_add_entities(
[
WhirlpoolSelectEntity(refrigerator, description)
for refrigerator in appliances_manager.refrigerators
for description in REFRIGERATOR_DESCRIPTIONS
]
)
class WhirlpoolSelectEntity(WhirlpoolEntity, SelectEntity):
"""Whirlpool select entity."""
def __init__(
self, appliance: Appliance, description: WhirlpoolSelectDescription
) -> None:
"""Initialize the select entity."""
super().__init__(appliance, unique_id_suffix=f"-{description.key}")
self.entity_description: WhirlpoolSelectDescription = description
@override
@property
def current_option(self) -> str | None:
"""Retrieve currently selected option."""
return self.entity_description.value_fn(self._appliance)
@override
async def async_select_option(self, option: str) -> None:
"""Set the selected option."""
try:
WhirlpoolSelectEntity._check_service_request(
await self.entity_description.set_fn(self._appliance, option)
)
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_value_set",
) from err

View File

@@ -46,6 +46,11 @@
}
},
"entity": {
"select": {
"refrigerator_temperature": {
"name": "Temperature"
}
},
"sensor": {
"dryer_state": {
"name": "[%key:component::whirlpool::entity::sensor::washer_state::name%]",
@@ -211,6 +216,9 @@
"appliances_fetch_failed": {
"message": "Failed to fetch appliances"
},
"invalid_value_set": {
"message": "Invalid value provided"
},
"request_failed": {
"message": "Request failed"
}

View File

@@ -3,6 +3,7 @@
import asyncio
from http import HTTPStatus
import logging
from typing import TYPE_CHECKING, Any
import aiohttp
import voluptuous as vol
@@ -11,6 +12,7 @@ from homeassistant.components.tts import (
CONF_LANG,
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
Provider,
TtsAudioType,
)
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import config_validation as cv
@@ -104,22 +106,26 @@ class YandexSpeechKitProvider(Provider):
self.name = "YandexTTS"
@property
def default_language(self):
def default_language(self) -> str:
"""Return the default language."""
return self._language
@property
def supported_languages(self):
def supported_languages(self) -> list[str]:
"""Return list of supported languages."""
return SUPPORT_LANGUAGES
@property
def supported_options(self):
def supported_options(self) -> list[str]:
"""Return list of supported options."""
return SUPPORTED_OPTIONS
async def async_get_tts_audio(self, message, language, options):
async def async_get_tts_audio(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Load TTS from yandex."""
if TYPE_CHECKING:
assert self.hass
websession = async_get_clientsession(self.hass)
actual_language = language

View File

@@ -7,7 +7,7 @@ LABS_PREVIEW_FEATURES = {
"analytics": {
"snapshots": {
"feedback_url": "https://forms.gle/GqvRmgmghSDco8M46",
"learn_more_url": "",
"learn_more_url": "https://www.home-assistant.io/blog/2026/02/02/about-device-database/",
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new",
},
},

View File

@@ -39,7 +39,7 @@ habluetooth==5.8.0
hass-nabucasa==1.12.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20260128.3
home-assistant-frontend==20260128.4
home-assistant-intents==2026.1.28
httpx==0.28.1
ifaddr==0.2.0

View File

@@ -1484,6 +1484,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = {
TypeHintMatch(
function_name="battery_level",
return_type=["int", None],
mandatory=True,
),
TypeHintMatch(
function_name="source_type",
@@ -1508,18 +1509,22 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = {
TypeHintMatch(
function_name="location_name",
return_type=["str", None],
mandatory=True,
),
TypeHintMatch(
function_name="latitude",
return_type=["float", None],
mandatory=True,
),
TypeHintMatch(
function_name="longitude",
return_type=["float", None],
mandatory=True,
),
TypeHintMatch(
function_name="state",
return_type=["str", None],
mandatory=True,
),
],
),
@@ -1529,14 +1534,17 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = {
TypeHintMatch(
function_name="ip_address",
return_type=["str", None],
mandatory=True,
),
TypeHintMatch(
function_name="mac_address",
return_type=["str", None],
mandatory=True,
),
TypeHintMatch(
function_name="hostname",
return_type=["str", None],
mandatory=True,
),
TypeHintMatch(
function_name="state",
@@ -2702,24 +2710,29 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = {
TypeHintMatch(
function_name="default_language",
return_type=["str", None],
mandatory=True,
),
TypeHintMatch(
function_name="supported_languages",
return_type=["list[str]", None],
mandatory=True,
),
TypeHintMatch(
function_name="supported_options",
return_type=["list[str]", None],
mandatory=True,
),
TypeHintMatch(
function_name="default_options",
return_type=["Mapping[str, Any]", None],
mandatory=True,
),
TypeHintMatch(
function_name="get_tts_audio",
arg_types={1: "str", 2: "str", 3: "dict[str, Any]"},
return_type="TtsAudioType",
has_async_counterpart=True,
mandatory=True,
),
],
),

4
requirements_all.txt generated
View File

@@ -1219,7 +1219,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260128.3
home-assistant-frontend==20260128.4
# homeassistant.components.conversation
home-assistant-intents==2026.1.28
@@ -1865,7 +1865,7 @@ pyElectra==1.2.4
pyEmby==1.10
# homeassistant.components.hikvision
pyHik==0.4.1
pyHik==0.4.2
# homeassistant.components.homee
pyHomee==1.3.8

View File

@@ -1077,7 +1077,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260128.3
home-assistant-frontend==20260128.4
# homeassistant.components.conversation
home-assistant-intents==2026.1.28
@@ -1602,7 +1602,7 @@ pyDuotecno==2024.10.1
pyElectra==1.2.4
# homeassistant.components.hikvision
pyHik==0.4.1
pyHik==0.4.2
# homeassistant.components.homee
pyHomee==1.3.8

View File

@@ -6,7 +6,8 @@ from aiohttp import ClientError
from blinkpy.auth import LoginError
import pytest
from homeassistant.components.blink.const import DOMAIN, SERVICE_SAVE_VIDEO
from homeassistant.components.blink.const import DOMAIN
from homeassistant.components.blink.services import SERVICE_SAVE_VIDEO
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant

View File

@@ -4,7 +4,8 @@ from unittest.mock import MagicMock
import pytest
from homeassistant.components.blink.const import DOMAIN, SERVICE_SEND_PIN
from homeassistant.components.blink.const import DOMAIN
from homeassistant.components.blink.services import SERVICE_SEND_PIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_PIN
from homeassistant.core import HomeAssistant

View File

@@ -10,11 +10,9 @@ import pytest
from homeassistant import core
from homeassistant.components import fan
from homeassistant.components.bond.const import (
DOMAIN,
SERVICE_SET_FAN_SPEED_TRACKED_STATE,
)
from homeassistant.components.bond.const import DOMAIN
from homeassistant.components.bond.fan import PRESET_MODE_BREEZE
from homeassistant.components.bond.services import SERVICE_SET_FAN_SPEED_TRACKED_STATE
from homeassistant.components.fan import (
ATTR_DIRECTION,
ATTR_PERCENTAGE,

View File

@@ -5,13 +5,11 @@ from datetime import timedelta
from bond_async import Action, DeviceType
import pytest
from homeassistant.components.bond.const import (
from homeassistant.components.bond.const import DOMAIN
from homeassistant.components.bond.services import (
ATTR_POWER_STATE,
DOMAIN,
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE,
SERVICE_SET_LIGHT_POWER_TRACKED_STATE,
)
from homeassistant.components.bond.light import (
SERVICE_START_DECREASING_BRIGHTNESS,
SERVICE_START_INCREASING_BRIGHTNESS,
SERVICE_STOP,

View File

@@ -5,9 +5,9 @@ from datetime import timedelta
from bond_async import Action, DeviceType
import pytest
from homeassistant.components.bond.const import (
from homeassistant.components.bond.const import DOMAIN
from homeassistant.components.bond.services import (
ATTR_POWER_STATE,
DOMAIN,
SERVICE_SET_POWER_TRACKED_STATE,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN

View File

@@ -6,7 +6,8 @@ from elgato import ElgatoError
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.elgato.const import DOMAIN, SERVICE_IDENTIFY
from homeassistant.components.elgato.const import DOMAIN
from homeassistant.components.elgato.services import SERVICE_IDENTIFY
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,

View File

@@ -36,6 +36,9 @@ MOCK_DEVICE_STATE = DeviceState(
name="Fridge",
type="fridge",
value=5,
target=4,
min=2,
max=8,
unit=TemperatureUnit.CELSIUS,
),
TemperatureControl(
@@ -44,6 +47,9 @@ MOCK_DEVICE_STATE = DeviceState(
name="Freezer",
type="freezer",
value=-18,
target=-18,
min=-24,
max=-16,
unit=TemperatureUnit.CELSIUS,
),
],
@@ -100,11 +106,13 @@ async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_liebherr_client: MagicMock,
platforms: list[Platform],
) -> MockConfigEntry:
"""Set up the Liebherr 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()
with patch("homeassistant.components.liebherr.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry

View File

@@ -0,0 +1,181 @@
# serializer version: 1
# name: test_numbers[number.test_fridge_bottom_zone_setpoint-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': -16,
'min': -24,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.test_fridge_bottom_zone_setpoint',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Bottom zone setpoint',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Bottom zone setpoint',
'platform': 'liebherr',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'setpoint_temperature_bottom_zone',
'unique_id': 'test_device_id_setpoint_temperature_2',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_numbers[number.test_fridge_bottom_zone_setpoint-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Test Fridge Bottom zone setpoint',
'max': -16,
'min': -24,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.test_fridge_bottom_zone_setpoint',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '-18',
})
# ---
# name: test_numbers[number.test_fridge_top_zone_setpoint-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 8,
'min': 2,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.test_fridge_top_zone_setpoint',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Top zone setpoint',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Top zone setpoint',
'platform': 'liebherr',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'setpoint_temperature_top_zone',
'unique_id': 'test_device_id_setpoint_temperature_1',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_numbers[number.test_fridge_top_zone_setpoint-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Test Fridge Top zone setpoint',
'max': 8,
'min': 2,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.test_fridge_top_zone_setpoint',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '4',
})
# ---
# name: test_single_zone_number[number.single_zone_fridge_setpoint-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 8,
'min': 2,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.single_zone_fridge_setpoint',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Setpoint',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Setpoint',
'platform': 'liebherr',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'setpoint_temperature',
'unique_id': 'single_zone_id_setpoint_temperature_1',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_single_zone_number[number.single_zone_fridge_setpoint-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Single Zone Fridge Setpoint',
'max': 8,
'min': 2,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.single_zone_fridge_setpoint',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '4',
})
# ---

View File

@@ -0,0 +1,321 @@
"""Test the Liebherr number platform."""
from datetime import timedelta
from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from pyliebherrhomeapi import (
Device,
DeviceState,
DeviceType,
TemperatureControl,
TemperatureUnit,
ZonePosition,
)
from pyliebherrhomeapi.exceptions import LiebherrConnectionError
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.number import (
ATTR_VALUE,
DEFAULT_MAX_VALUE,
DEFAULT_MIN_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from .conftest import MOCK_DEVICE
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return [Platform.NUMBER]
@pytest.fixture(autouse=True)
def enable_all_entities(entity_registry_enabled_by_default: None) -> None:
"""Make sure all entities are enabled."""
@pytest.mark.usefixtures("init_integration")
async def test_numbers(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test all number entities with multi-zone device."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_single_zone_number(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_liebherr_client: MagicMock,
mock_config_entry: MockConfigEntry,
platforms: list[Platform],
) -> None:
"""Test single zone device uses device name without zone suffix."""
device = Device(
device_id="single_zone_id",
nickname="Single Zone Fridge",
device_type=DeviceType.FRIDGE,
device_name="K2601",
)
mock_liebherr_client.get_devices.return_value = [device]
mock_liebherr_client.get_device_state.return_value = DeviceState(
device=device,
controls=[
TemperatureControl(
zone_id=1,
zone_position=ZonePosition.TOP,
name="Fridge",
type="fridge",
value=4,
target=4,
min=2,
max=8,
unit=TemperatureUnit.CELSIUS,
)
],
)
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.liebherr.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_multi_zone_with_none_position(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_liebherr_client: MagicMock,
mock_config_entry: MockConfigEntry,
platforms: list[Platform],
) -> None:
"""Test multi-zone device with None zone_position falls back to base translation key."""
device = Device(
device_id="multi_zone_none",
nickname="Multi Zone Fridge",
device_type=DeviceType.COMBI,
device_name="CBNes9999",
)
mock_liebherr_client.get_devices.return_value = [device]
mock_liebherr_client.get_device_state.return_value = DeviceState(
device=device,
controls=[
TemperatureControl(
zone_id=1,
zone_position=None, # None triggers fallback
name="Fridge",
type="fridge",
value=5,
target=4,
min=2,
max=8,
unit=TemperatureUnit.CELSIUS,
),
TemperatureControl(
zone_id=2,
zone_position=ZonePosition.BOTTOM,
name="Freezer",
type="freezer",
value=-18,
target=-18,
min=-24,
max=-16,
unit=TemperatureUnit.CELSIUS,
),
],
)
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.liebherr.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Zone with None position should have base translation key
zone1_entity = entity_registry.async_get("number.multi_zone_fridge_setpoint")
assert zone1_entity is not None
assert zone1_entity.translation_key == "setpoint_temperature"
# Zone with valid position should have zone-specific translation key
zone2_entity = entity_registry.async_get(
"number.multi_zone_fridge_bottom_zone_setpoint"
)
assert zone2_entity is not None
assert zone2_entity.translation_key == "setpoint_temperature_bottom_zone"
@pytest.mark.usefixtures("init_integration")
async def test_set_temperature(
hass: HomeAssistant,
mock_liebherr_client: MagicMock,
) -> None:
"""Test setting the temperature."""
entity_id = "number.test_fridge_top_zone_setpoint"
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 6},
blocking=True,
)
mock_liebherr_client.set_temperature.assert_called_once_with(
device_id="test_device_id",
zone_id=1,
target=6,
unit=TemperatureUnit.CELSIUS,
)
@pytest.mark.usefixtures("init_integration")
async def test_set_temperature_failure(
hass: HomeAssistant,
mock_liebherr_client: MagicMock,
) -> None:
"""Test setting temperature fails gracefully."""
entity_id = "number.test_fridge_top_zone_setpoint"
mock_liebherr_client.set_temperature.side_effect = LiebherrConnectionError(
"Connection failed"
)
with pytest.raises(HomeAssistantError, match="Failed to set temperature"):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 6},
blocking=True,
)
@pytest.mark.usefixtures("init_integration")
async def test_number_update_failure(
hass: HomeAssistant,
mock_liebherr_client: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test number becomes unavailable when coordinator update fails and recovers."""
entity_id = "number.test_fridge_top_zone_setpoint"
# Initial state should be available with value
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "4"
# Simulate update error
mock_liebherr_client.get_device_state.side_effect = LiebherrConnectionError(
"Connection failed"
)
# Advance time to trigger coordinator refresh (60 second interval)
freezer.tick(timedelta(seconds=61))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Number should now be unavailable
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_UNAVAILABLE
# Simulate recovery
mock_liebherr_client.get_device_state.side_effect = None
freezer.tick(timedelta(seconds=61))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Number should recover
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "4"
@pytest.mark.usefixtures("init_integration")
async def test_number_when_control_missing(
hass: HomeAssistant,
mock_liebherr_client: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test number entity behavior when temperature control is removed."""
entity_id = "number.test_fridge_top_zone_setpoint"
# Initial values should be from the control
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "4"
assert state.attributes["min"] == 2
assert state.attributes["max"] == 8
assert state.attributes["unit_of_measurement"] == "°C"
# Device stops reporting controls
mock_liebherr_client.get_device_state.return_value = DeviceState(
device=MOCK_DEVICE, controls=[]
)
# Advance time to trigger coordinator refresh
freezer.tick(timedelta(seconds=61))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# State should be unavailable
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_UNAVAILABLE
async def test_number_with_none_min_max(
hass: HomeAssistant,
mock_liebherr_client: MagicMock,
mock_config_entry: MockConfigEntry,
platforms: list[Platform],
) -> None:
"""Test number entity returns defaults when control has None min/max."""
device = Device(
device_id="none_min_max_device",
nickname="Test Fridge",
device_type=DeviceType.FRIDGE,
device_name="K2601",
)
mock_liebherr_client.get_devices.return_value = [device]
mock_liebherr_client.get_device_state.return_value = DeviceState(
device=device,
controls=[
TemperatureControl(
zone_id=1,
zone_position=ZonePosition.TOP,
name="Fridge",
type="fridge",
value=4,
target=4,
min=None, # None min
max=None, # None max
unit=TemperatureUnit.CELSIUS,
)
],
)
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.liebherr.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
entity_id = "number.test_fridge_setpoint"
state = hass.states.get(entity_id)
assert state is not None
# Should return defaults when min/max are None
assert state.attributes["min"] == DEFAULT_MIN_VALUE
assert state.attributes["max"] == DEFAULT_MAX_VALUE

View File

@@ -1,7 +1,7 @@
"""Test the Liebherr sensor platform."""
from datetime import timedelta
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from pyliebherrhomeapi import (
@@ -22,7 +22,7 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components.liebherr.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -49,6 +49,7 @@ async def test_single_zone_sensor(
entity_registry: er.EntityRegistry,
mock_liebherr_client: MagicMock,
mock_config_entry: MockConfigEntry,
platforms: list[Platform],
) -> None:
"""Test single zone device uses device name without zone suffix."""
device = Device(
@@ -73,8 +74,9 @@ async def test_single_zone_sensor(
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
with patch("homeassistant.components.liebherr.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@@ -248,9 +250,3 @@ async def test_sensor_unavailable_when_control_missing(
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_UNAVAILABLE
# Verify entity properties return None when control is missing
entity = hass.data["entity_components"]["sensor"].get_entity(entity_id)
assert entity is not None
assert entity.native_value is None
assert entity.native_unit_of_measurement is None

View File

@@ -8,7 +8,7 @@ from unittest.mock import MagicMock
from pylitterbot import Robot
import pytest
from homeassistant.components.litterrobot.vacuum import SERVICE_SET_SLEEP_MODE
from homeassistant.components.litterrobot.services import SERVICE_SET_SLEEP_MODE
from homeassistant.components.vacuum import (
DOMAIN as VACUUM_DOMAIN,
SERVICE_START,

View File

@@ -74,7 +74,6 @@ async def integration_fixture(
@pytest.fixture(
params=[
"air_purifier",
"air_quality_sensor",
"aqara_door_window_p2",
"aqara_motion_p2",
@@ -101,7 +100,9 @@ async def integration_fixture(
"ikea_air_quality_monitor",
"ikea_scroll_wheel",
"inovelli_vtm30",
"inovelli_vtm31",
"longan_link_thermostat",
"mock_air_purifier",
"mock_battery_storage",
"mock_cooktop",
"mock_dimmable_light",
@@ -141,7 +142,6 @@ async def integration_fixture(
"mock_window_covering_pa_lift",
"mock_window_covering_pa_tilt",
"mock_window_covering_tilt",
"multi_endpoint_light",
"onoff_light_with_levelcontrol_present",
"resideo_x2s_thermostat",
"secuyou_smart_lock",

View File

@@ -41,7 +41,7 @@
"0/40/0": 17,
"0/40/1": "TEST_VENDOR",
"0/40/2": 65521,
"0/40/3": "Air Purifier",
"0/40/3": "Mock Air Purifier",
"0/40/4": 32769,
"0/40/5": "",
"0/40/6": "**REDACTED**",

View File

@@ -1,102 +1,4 @@
# serializer version: 1
# name: test_buttons[air_purifier][button.air_purifier_reset_filter_condition-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.air_purifier_reset_filter_condition',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Reset filter condition',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset filter condition',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'reset_filter_condition',
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-HepaFilterMonitoringResetButton-113-65529',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[air_purifier][button.air_purifier_reset_filter_condition-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Air Purifier Reset filter condition',
}),
'context': <ANY>,
'entity_id': 'button.air_purifier_reset_filter_condition',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[air_purifier][button.air_purifier_reset_filter_condition_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.air_purifier_reset_filter_condition_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Reset filter condition',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset filter condition',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'reset_filter_condition',
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-ActivatedCarbonFilterMonitoringResetButton-114-65529',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[air_purifier][button.air_purifier_reset_filter_condition_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Air Purifier Reset filter condition',
}),
'context': <ANY>,
'entity_id': 'button.air_purifier_reset_filter_condition_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[aqara_door_window_p2][button.aqara_door_and_window_sensor_p2_identify-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -1847,6 +1749,306 @@
'state': 'unknown',
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (1)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (1)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (1)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (2)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (2)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-2-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (2)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_6-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_6',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (6)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (6)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_6-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (6)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_6',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_config-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_config',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (Config)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (Config)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_config-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (Config)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_config',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_down-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_down',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (Down)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (Down)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_down-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (Down)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_down',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_up-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_up',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (Up)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (Up)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[inovelli_vtm31][button.inovelli_identify_up-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (Up)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_up',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[longan_link_thermostat][button.longan_link_hvac_identify-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -2291,6 +2493,104 @@
'state': 'unknown',
})
# ---
# name: test_buttons[mock_air_purifier][button.mock_air_purifier_reset_filter_condition-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.mock_air_purifier_reset_filter_condition',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Reset filter condition',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset filter condition',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'reset_filter_condition',
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-HepaFilterMonitoringResetButton-113-65529',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[mock_air_purifier][button.mock_air_purifier_reset_filter_condition-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Air Purifier Reset filter condition',
}),
'context': <ANY>,
'entity_id': 'button.mock_air_purifier_reset_filter_condition',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[mock_air_purifier][button.mock_air_purifier_reset_filter_condition_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.mock_air_purifier_reset_filter_condition_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Reset filter condition',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset filter condition',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'reset_filter_condition',
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-ActivatedCarbonFilterMonitoringResetButton-114-65529',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[mock_air_purifier][button.mock_air_purifier_reset_filter_condition_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Air Purifier Reset filter condition',
}),
'context': <ANY>,
'entity_id': 'button.mock_air_purifier_reset_filter_condition_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[mock_lock][button.mock_lock_identify-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -2887,306 +3187,6 @@
'state': 'unknown',
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (1)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (1)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (1)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (2)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (2)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-2-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (2)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_6-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_6',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (6)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (6)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_6-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (6)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_6',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_config-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_config',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (Config)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (Config)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_config-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (Config)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_config',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_down-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_down',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (Down)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (Down)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_down-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (Down)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_down',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_up-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.inovelli_identify_up',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify (Up)',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (Up)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_up-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Inovelli Identify (Up)',
}),
'context': <ANY>,
'entity_id': 'button.inovelli_identify_up',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[onoff_light_with_levelcontrol_present][button.d215s_identify-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -1,69 +1,4 @@
# serializer version: 1
# name: test_climates[air_purifier][climate.air_purifier-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 30.0,
'min_temp': 5.0,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.air_purifier',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <ClimateEntityFeature: 385>,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-5-MatterThermostat-513-0',
'unit_of_measurement': None,
})
# ---
# name: test_climates[air_purifier][climate.air_purifier-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 20.0,
'friendly_name': 'Air Purifier',
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 30.0,
'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 385>,
'temperature': 20.0,
}),
'context': <ANY>,
'entity_id': 'climate.air_purifier',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_climates[aqara_thermostat_w500][climate.floor_heating_thermostat-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -395,6 +330,71 @@
'state': 'cool',
})
# ---
# name: test_climates[mock_air_purifier][climate.mock_air_purifier-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 30.0,
'min_temp': 5.0,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.mock_air_purifier',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <ClimateEntityFeature: 385>,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-5-MatterThermostat-513-0',
'unit_of_measurement': None,
})
# ---
# name: test_climates[mock_air_purifier][climate.mock_air_purifier-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 20.0,
'friendly_name': 'Mock Air Purifier',
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 30.0,
'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 385>,
'temperature': 20.0,
}),
'context': <ANY>,
'entity_id': 'climate.mock_air_purifier',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_climates[mock_room_airconditioner][climate.room_airconditioner-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -1415,6 +1415,216 @@
'state': 'unknown',
})
# ---
# name: test_events[inovelli_vtm31][event.inovelli_button_config-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'event',
'entity_category': None,
'entity_id': 'event.inovelli_button_config',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Button (Config)',
'options': dict({
}),
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
'original_icon': None,
'original_name': 'Button (Config)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'button',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-GenericSwitch-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_events[inovelli_vtm31][event.inovelli_button_config-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'button',
'event_type': None,
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
'friendly_name': 'Inovelli Button (Config)',
}),
'context': <ANY>,
'entity_id': 'event.inovelli_button_config',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_events[inovelli_vtm31][event.inovelli_button_down-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'event',
'entity_category': None,
'entity_id': 'event.inovelli_button_down',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Button (Down)',
'options': dict({
}),
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
'original_icon': None,
'original_name': 'Button (Down)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'button',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-GenericSwitch-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_events[inovelli_vtm31][event.inovelli_button_down-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'button',
'event_type': None,
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
'friendly_name': 'Inovelli Button (Down)',
}),
'context': <ANY>,
'entity_id': 'event.inovelli_button_down',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_events[inovelli_vtm31][event.inovelli_button_up-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'event',
'entity_category': None,
'entity_id': 'event.inovelli_button_up',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Button (Up)',
'options': dict({
}),
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
'original_icon': None,
'original_name': 'Button (Up)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'button',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-GenericSwitch-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_events[inovelli_vtm31][event.inovelli_button_up-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'button',
'event_type': None,
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
'friendly_name': 'Inovelli Button (Up)',
}),
'context': <ANY>,
'entity_id': 'event.inovelli_button_up',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_events[mock_generic_switch][event.mock_generic_switch_button-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -1611,216 +1821,6 @@
'state': 'unknown',
})
# ---
# name: test_events[multi_endpoint_light][event.inovelli_button_config-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'event',
'entity_category': None,
'entity_id': 'event.inovelli_button_config',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Button (Config)',
'options': dict({
}),
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
'original_icon': None,
'original_name': 'Button (Config)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'button',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-GenericSwitch-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_events[multi_endpoint_light][event.inovelli_button_config-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'button',
'event_type': None,
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
'friendly_name': 'Inovelli Button (Config)',
}),
'context': <ANY>,
'entity_id': 'event.inovelli_button_config',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_events[multi_endpoint_light][event.inovelli_button_down-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'event',
'entity_category': None,
'entity_id': 'event.inovelli_button_down',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Button (Down)',
'options': dict({
}),
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
'original_icon': None,
'original_name': 'Button (Down)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'button',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-GenericSwitch-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_events[multi_endpoint_light][event.inovelli_button_down-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'button',
'event_type': None,
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
'friendly_name': 'Inovelli Button (Down)',
}),
'context': <ANY>,
'entity_id': 'event.inovelli_button_down',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_events[multi_endpoint_light][event.inovelli_button_up-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'event',
'entity_category': None,
'entity_id': 'event.inovelli_button_up',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Button (Up)',
'options': dict({
}),
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
'original_icon': None,
'original_name': 'Button (Up)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'button',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-GenericSwitch-59-1',
'unit_of_measurement': None,
})
# ---
# name: test_events[multi_endpoint_light][event.inovelli_button_up-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'button',
'event_type': None,
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'multi_press_5',
'long_press',
'long_release',
]),
'friendly_name': 'Inovelli Button (Up)',
}),
'context': <ANY>,
'entity_id': 'event.inovelli_button_up',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_events[silabs_light_switch][event.light_switch_example_button-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -1,76 +1,4 @@
# serializer version: 1
# name: test_fans[air_purifier][fan.air_purifier-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'preset_modes': list([
'low',
'medium',
'high',
'auto',
'natural_wind',
'sleep_wind',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'fan',
'entity_category': None,
'entity_id': 'fan.air_purifier',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <FanEntityFeature: 63>,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-MatterFan-514-0',
'unit_of_measurement': None,
})
# ---
# name: test_fans[air_purifier][fan.air_purifier-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'direction': 'forward',
'friendly_name': 'Air Purifier',
'oscillating': False,
'percentage': None,
'percentage_step': 10.0,
'preset_mode': 'auto',
'preset_modes': list([
'low',
'medium',
'high',
'auto',
'natural_wind',
'sleep_wind',
]),
'supported_features': <FanEntityFeature: 63>,
}),
'context': <ANY>,
'entity_id': 'fan.air_purifier',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_fans[longan_link_thermostat][fan.longan_link_hvac-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -135,6 +63,78 @@
'state': 'off',
})
# ---
# name: test_fans[mock_air_purifier][fan.mock_air_purifier-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'preset_modes': list([
'low',
'medium',
'high',
'auto',
'natural_wind',
'sleep_wind',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'fan',
'entity_category': None,
'entity_id': 'fan.mock_air_purifier',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <FanEntityFeature: 63>,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-MatterFan-514-0',
'unit_of_measurement': None,
})
# ---
# name: test_fans[mock_air_purifier][fan.mock_air_purifier-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'direction': 'forward',
'friendly_name': 'Mock Air Purifier',
'oscillating': False,
'percentage': None,
'percentage_step': 10.0,
'preset_mode': 'auto',
'preset_modes': list([
'low',
'medium',
'high',
'auto',
'natural_wind',
'sleep_wind',
]),
'supported_features': <FanEntityFeature: 63>,
}),
'context': <ANY>,
'entity_id': 'fan.mock_air_purifier',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_fans[mock_extractor_hood][fan.mock_extractor_hood-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -243,6 +243,141 @@
'state': 'off',
})
# ---
# name: test_lights[inovelli_vtm31][light.inovelli_light_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.inovelli_light_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light (1)',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light (1)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <LightEntityFeature: 32>,
'translation_key': 'light',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-MatterLight-6-0',
'unit_of_measurement': None,
})
# ---
# name: test_lights[inovelli_vtm31][light.inovelli_light_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'brightness': None,
'color_mode': None,
'friendly_name': 'Inovelli Light (1)',
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
'supported_features': <LightEntityFeature: 32>,
}),
'context': <ANY>,
'entity_id': 'light.inovelli_light_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_lights[inovelli_vtm31][light.inovelli_light_6-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>,
<ColorMode.XY: 'xy'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.inovelli_light_6',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light (6)',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light (6)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <LightEntityFeature: 32>,
'translation_key': 'light',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-MatterLight-6-0',
'unit_of_measurement': None,
})
# ---
# name: test_lights[inovelli_vtm31][light.inovelli_light_6-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'brightness': None,
'color_mode': None,
'color_temp': None,
'color_temp_kelvin': None,
'friendly_name': 'Inovelli Light (6)',
'hs_color': None,
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'rgb_color': None,
'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>,
<ColorMode.XY: 'xy'>,
]),
'supported_features': <LightEntityFeature: 32>,
'xy_color': None,
}),
'context': <ANY>,
'entity_id': 'light.inovelli_light_6',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_lights[mock_dimmable_light][light.mock_dimmable_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -628,141 +763,6 @@
'state': 'on',
})
# ---
# name: test_lights[multi_endpoint_light][light.inovelli_light_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.inovelli_light_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light (1)',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light (1)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <LightEntityFeature: 32>,
'translation_key': 'light',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-MatterLight-6-0',
'unit_of_measurement': None,
})
# ---
# name: test_lights[multi_endpoint_light][light.inovelli_light_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'brightness': None,
'color_mode': None,
'friendly_name': 'Inovelli Light (1)',
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
'supported_features': <LightEntityFeature: 32>,
}),
'context': <ANY>,
'entity_id': 'light.inovelli_light_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_lights[multi_endpoint_light][light.inovelli_light_6-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>,
<ColorMode.XY: 'xy'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.inovelli_light_6',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light (6)',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light (6)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <LightEntityFeature: 32>,
'translation_key': 'light',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-MatterLight-6-0',
'unit_of_measurement': None,
})
# ---
# name: test_lights[multi_endpoint_light][light.inovelli_light_6-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'brightness': None,
'color_mode': None,
'color_temp': None,
'color_temp_kelvin': None,
'friendly_name': 'Inovelli Light (6)',
'hs_color': None,
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'rgb_color': None,
'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>,
<ColorMode.XY: 'xy'>,
]),
'supported_features': <LightEntityFeature: 32>,
'xy_color': None,
}),
'context': <ANY>,
'entity_id': 'light.inovelli_light_6',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_lights[onoff_light_with_levelcontrol_present][light.d215s-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -1116,6 +1116,415 @@
'state': 'unknown',
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_led_off_intensity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 75,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_led_off_intensity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'LED off intensity',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'LED off intensity',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'led_indicator_intensity_off',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-InovelliLEDIndicatorIntensityOff-305134641-305070178',
'unit_of_measurement': None,
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_led_off_intensity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli LED off intensity',
'max': 75,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_led_off_intensity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_led_on_intensity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 75,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_led_on_intensity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'LED on intensity',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'LED on intensity',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'led_indicator_intensity_on',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-InovelliLEDIndicatorIntensityOn-305134641-305070177',
'unit_of_measurement': None,
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_led_on_intensity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli LED on intensity',
'max': 75,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_led_on_intensity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '33',
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_off_transition_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_off_transition_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Off transition time',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Off transition time',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'off_transition_time',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-off_transition_time-8-19',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_off_transition_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli Off transition time',
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_off_transition_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.5',
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_on_level_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 255,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_on_level_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'On level (1)',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'On level (1)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'on_level',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-on_level-8-17',
'unit_of_measurement': None,
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_on_level_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli On level (1)',
'max': 255,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_on_level_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '137',
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_on_level_6-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 255,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_on_level_6',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'On level (6)',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'On level (6)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'on_level',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-on_level-8-17',
'unit_of_measurement': None,
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_on_level_6-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli On level (6)',
'max': 255,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_on_level_6',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '254',
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_on_off_transition_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_on_off_transition_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'On/Off transition time',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'On/Off transition time',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'on_off_transition_time',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-on_off_transition_time-8-16',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_on_off_transition_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli On/Off transition time',
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_on_off_transition_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.5',
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_on_transition_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_on_transition_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'On transition time',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'On transition time',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'on_transition_time',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-on_transition_time-8-18',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_numbers[inovelli_vtm31][number.inovelli_on_transition_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli On transition time',
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_on_transition_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.5',
})
# ---
# name: test_numbers[mock_dimmable_light][number.mock_dimmable_light_off_transition_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -3113,415 +3522,6 @@
'state': '0',
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_led_off_intensity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 75,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_led_off_intensity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'LED off intensity',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'LED off intensity',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'led_indicator_intensity_off',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-InovelliLEDIndicatorIntensityOff-305134641-305070178',
'unit_of_measurement': None,
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_led_off_intensity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli LED off intensity',
'max': 75,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_led_off_intensity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_led_on_intensity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 75,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_led_on_intensity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'LED on intensity',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'LED on intensity',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'led_indicator_intensity_on',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-InovelliLEDIndicatorIntensityOn-305134641-305070177',
'unit_of_measurement': None,
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_led_on_intensity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli LED on intensity',
'max': 75,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_led_on_intensity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '33',
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_off_transition_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_off_transition_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Off transition time',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Off transition time',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'off_transition_time',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-off_transition_time-8-19',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_off_transition_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli Off transition time',
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_off_transition_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.5',
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_on_level_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 255,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_on_level_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'On level (1)',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'On level (1)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'on_level',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-on_level-8-17',
'unit_of_measurement': None,
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_on_level_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli On level (1)',
'max': 255,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_on_level_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '137',
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_on_level_6-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 255,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_on_level_6',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'On level (6)',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'On level (6)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'on_level',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-on_level-8-17',
'unit_of_measurement': None,
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_on_level_6-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli On level (6)',
'max': 255,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_on_level_6',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '254',
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_on_off_transition_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_on_off_transition_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'On/Off transition time',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'On/Off transition time',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'on_off_transition_time',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-on_off_transition_time-8-16',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_on_off_transition_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli On/Off transition time',
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_on_off_transition_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.5',
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_on_transition_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.inovelli_on_transition_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'On transition time',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'On transition time',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'on_transition_time',
'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-on_transition_time-8-18',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_numbers[multi_endpoint_light][number.inovelli_on_transition_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Inovelli On transition time',
'max': 65534,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.inovelli_on_transition_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.5',
})
# ---
# name: test_numbers[onoff_light_with_levelcontrol_present][number.d215s_on_level-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -146,7 +146,7 @@ async def test_node_added_subscription(
@pytest.mark.usefixtures("matter_node")
@pytest.mark.parametrize("node_fixture", ["air_purifier"])
@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_device_registry_single_node_composed_device(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
@@ -156,7 +156,7 @@ async def test_device_registry_single_node_composed_device(
@pytest.mark.usefixtures("matter_node")
@pytest.mark.parametrize("node_fixture", ["multi_endpoint_light"])
@pytest.mark.parametrize("node_fixture", ["inovelli_vtm31"])
async def test_multi_endpoint_name(hass: HomeAssistant) -> None:
"""Test that the entity name gets postfixed if the device has multiple primary endpoints."""
entity_state = hass.states.get("light.inovelli_light_1")

View File

@@ -44,14 +44,14 @@ async def test_fans(
snapshot_matter_entities(hass, entity_registry, snapshot, Platform.FAN)
@pytest.mark.parametrize("node_fixture", ["air_purifier"])
@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_fan_base(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test Fan platform."""
entity_id = "fan.air_purifier"
entity_id = "fan.mock_air_purifier"
state = hass.states.get(entity_id)
assert state
assert state.attributes["preset_modes"] == [
@@ -112,14 +112,14 @@ async def test_fan_base(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("node_fixture", ["air_purifier"])
@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_fan_turn_on_with_percentage(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test turning on the fan with a specific percentage."""
entity_id = "fan.air_purifier"
entity_id = "fan.mock_air_purifier"
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_TURN_ON,
@@ -226,14 +226,14 @@ async def test_fan_turn_on_with_preset_mode(
)
@pytest.mark.parametrize("node_fixture", ["air_purifier"])
@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_fan_turn_off(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test turning off the fan."""
entity_id = "fan.air_purifier"
entity_id = "fan.mock_air_purifier"
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_TURN_OFF,
@@ -269,14 +269,14 @@ async def test_fan_turn_off(
)
@pytest.mark.parametrize("node_fixture", ["air_purifier"])
@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_fan_oscillate(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test oscillating the fan."""
entity_id = "fan.air_purifier"
entity_id = "fan.mock_air_purifier"
for oscillating, value in ((True, 1), (False, 0)):
await hass.services.async_call(
FAN_DOMAIN,
@@ -293,14 +293,14 @@ async def test_fan_oscillate(
matter_client.write_attribute.reset_mock()
@pytest.mark.parametrize("node_fixture", ["air_purifier"])
@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_fan_set_direction(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test oscillating the fan."""
entity_id = "fan.air_purifier"
entity_id = "fan.mock_air_purifier"
for direction, value in ((DIRECTION_FORWARD, 0), (DIRECTION_REVERSE, 1)):
await hass.services.async_call(
FAN_DOMAIN,

View File

@@ -6,8 +6,8 @@ import pytest
from reolink_aio.exceptions import ReolinkError
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.components.reolink.button import ATTR_SPEED, SERVICE_PTZ_MOVE
from homeassistant.components.reolink.const import DOMAIN
from homeassistant.components.reolink.services import ATTR_SPEED, SERVICE_PTZ_MOVE
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant

View File

@@ -10,7 +10,7 @@ from syrupy.assertion import SnapshotAssertion
from vacuum_map_parser_base.map_data import Point
from homeassistant.components.roborock import DOMAIN
from homeassistant.components.roborock.const import (
from homeassistant.components.roborock.services import (
GET_MAPS_SERVICE_NAME,
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,

View File

@@ -26,7 +26,7 @@
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': '123456',
'sw_version': None,
'sw_version': '23.44.0 eb113390',
'via_device_id': None,
})
# ---
@@ -52,12 +52,12 @@
}),
'manufacturer': 'Tesla',
'model': 'Model 3',
'model_id': None,
'model_id': '3',
'name': 'Test',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': 'LRW3F7EK4NC700000',
'sw_version': None,
'sw_version': '2026.0.0',
'via_device_id': None,
})
# ---

View File

@@ -2362,68 +2362,6 @@
'state': '40.727',
})
# ---
# name: test_sensors[sensor.energy_site_version-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.energy_site_version',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Version',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Version',
'platform': 'teslemetry',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'version',
'unique_id': '123456-version',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.energy_site_version-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Energy Site Version',
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_version',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '23.44.0 eb113390',
})
# ---
# name: test_sensors[sensor.energy_site_version-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Energy Site Version',
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_version',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '23.44.0 eb113390',
})
# ---
# name: test_sensors[sensor.energy_site_vpp_backup_reserve-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -21,6 +21,7 @@ from homeassistant.components.teslemetry.const import CLIENT_ID, DOMAIN
# Coordinator constants
from homeassistant.components.teslemetry.coordinator import (
ENERGY_HISTORY_INTERVAL,
ENERGY_INFO_INTERVAL,
ENERGY_LIVE_INTERVAL,
VEHICLE_INTERVAL,
)
@@ -42,7 +43,9 @@ from .const import (
ENERGY_HISTORY,
LIVE_STATUS,
PRODUCTS_MODERN,
SITE_INFO,
UNIQUE_ID,
VEHICLE_DATA,
VEHICLE_DATA_ALT,
)
@@ -644,3 +647,136 @@ async def test_live_status_generic_error(
# Entry stays loaded but coordinator will have failed
assert entry.state is ConfigEntryState.LOADED
async def test_vehicle_streaming_version_update(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test vehicle sw_version is updated when streaming reports new version."""
# Track listen_Version calls
version_listeners: list = []
def mock_listen_version(callback):
version_listeners.append(callback)
return lambda: None # Return unsubscribe function
with patch(
"teslemetry_stream.TeslemetryStreamVehicle.listen_Version",
side_effect=mock_listen_version,
):
entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.LOADED
# Check initial device sw_version
vin = "LRW3F7EK4NC700000"
device = device_registry.async_get_device(identifiers={(DOMAIN, vin)})
assert device is not None
assert device.sw_version == "2026.0.0"
# Simulate streaming version update
assert len(version_listeners) > 0
version_listeners[0]("2026.1.0 abc123")
await hass.async_block_till_done()
# Check device sw_version was updated (build hash removed)
device = device_registry.async_get_device(identifiers={(DOMAIN, vin)})
assert device is not None
assert device.sw_version == "2026.1.0"
async def test_vehicle_streaming_version_update_ignores_none(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test vehicle sw_version is not updated when streaming reports None."""
version_listeners: list = []
def mock_listen_version(callback):
version_listeners.append(callback)
return lambda: None
with patch(
"teslemetry_stream.TeslemetryStreamVehicle.listen_Version",
side_effect=mock_listen_version,
):
entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.LOADED
vin = "LRW3F7EK4NC700000"
device = device_registry.async_get_device(identifiers={(DOMAIN, vin)})
assert device is not None
original_version = device.sw_version
# Simulate streaming version update with None
assert len(version_listeners) > 0
version_listeners[0](None)
await hass.async_block_till_done()
# Check device sw_version was not changed
device = device_registry.async_get_device(identifiers={(DOMAIN, vin)})
assert device is not None
assert device.sw_version == original_version
async def test_vehicle_polling_version_update(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_vehicle_data: AsyncMock,
mock_legacy: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test vehicle sw_version is updated when polling coordinator receives new version."""
entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.LOADED
vin = "LRW3F7EK4NC700000"
device = device_registry.async_get_device(identifiers={(DOMAIN, vin)})
assert device is not None
assert device.sw_version == "2026.0.0"
# Update mock to return new version on next poll
updated_vehicle_data = deepcopy(VEHICLE_DATA)
updated_vehicle_data["response"]["vehicle_state"]["car_version"] = "2026.2.0 def456"
mock_vehicle_data.return_value = updated_vehicle_data
# Trigger coordinator refresh
freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Check device sw_version was updated (build hash removed)
device = device_registry.async_get_device(identifiers={(DOMAIN, vin)})
assert device is not None
assert device.sw_version == "2026.2.0"
async def test_energy_site_version_update(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_site_info: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test energy site sw_version is updated when info coordinator receives new version."""
entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.LOADED
site_id = "123456"
device = device_registry.async_get_device(identifiers={(DOMAIN, site_id)})
assert device is not None
assert device.sw_version == "23.44.0 eb113390"
# Update mock to return new version on next poll
updated_site_info = deepcopy(SITE_INFO)
updated_site_info["response"]["version"] = "24.1.0 abc123"
mock_site_info.side_effect = lambda: updated_site_info
# Trigger coordinator refresh
freezer.tick(ENERGY_INFO_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Check device sw_version was updated
device = device_registry.async_get_device(identifiers={(DOMAIN, site_id)})
assert device is not None
assert device.sw_version == "24.1.0 abc123"

Some files were not shown because too many files have changed in this diff Show More