mirror of
https://github.com/home-assistant/core.git
synced 2026-02-06 15:25:33 +01:00
Compare commits
9 Commits
epenet/202
...
LocalTempe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
857fb61bb0 | ||
|
|
77155b3eca | ||
|
|
babcb80b9f | ||
|
|
9831fd9c14 | ||
|
|
296487440e | ||
|
|
34ebf73741 | ||
|
|
ef9d80cae2 | ||
|
|
b03fb3e179 | ||
|
|
3112e37acc |
@@ -10,7 +10,6 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for Baidu speech service."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aip import AipSpeech
|
||||
import voluptuous as vol
|
||||
@@ -10,7 +9,6 @@ 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
|
||||
@@ -87,17 +85,17 @@ class BaiduTTSProvider(Provider):
|
||||
}
|
||||
|
||||
@property
|
||||
def default_language(self) -> str:
|
||||
def default_language(self):
|
||||
"""Return the default language."""
|
||||
return self._lang
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
def supported_languages(self):
|
||||
"""Return a list of supported languages."""
|
||||
return SUPPORTED_LANGUAGES
|
||||
|
||||
@property
|
||||
def default_options(self) -> dict[str, Any]:
|
||||
def default_options(self):
|
||||
"""Return a dict including default options."""
|
||||
return {
|
||||
CONF_PERSON: self._speech_conf_data[_OPTIONS[CONF_PERSON]],
|
||||
@@ -107,16 +105,11 @@ class BaiduTTSProvider(Provider):
|
||||
}
|
||||
|
||||
@property
|
||||
def supported_options(self) -> list[str]:
|
||||
def supported_options(self):
|
||||
"""Return a list of supported options."""
|
||||
return SUPPORTED_OPTIONS
|
||||
|
||||
def get_tts_audio(
|
||||
self,
|
||||
message: str,
|
||||
language: str,
|
||||
options: dict[str, Any],
|
||||
) -> TtsAudioType:
|
||||
def get_tts_audio(self, message, language, options):
|
||||
"""Load TTS from BaiduTTS."""
|
||||
|
||||
aip_speech = AipSpeech(
|
||||
|
||||
@@ -16,17 +16,14 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers import 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,
|
||||
@@ -41,12 +38,6 @@ _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]
|
||||
|
||||
@@ -5,3 +5,10 @@ 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"
|
||||
|
||||
@@ -8,6 +8,7 @@ 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,
|
||||
@@ -17,6 +18,7 @@ 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,
|
||||
@@ -25,6 +27,7 @@ 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
|
||||
@@ -41,6 +44,12 @@ 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)
|
||||
|
||||
@@ -7,20 +7,37 @@ 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,
|
||||
@@ -31,6 +48,14 @@ 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
|
||||
@@ -69,6 +94,22 @@ 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,
|
||||
)
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
"""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",
|
||||
)
|
||||
@@ -6,13 +6,16 @@ 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
|
||||
|
||||
|
||||
@@ -23,6 +26,12 @@ 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)
|
||||
|
||||
@@ -2,23 +2,12 @@
|
||||
|
||||
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)
|
||||
|
||||
@@ -14,3 +14,6 @@ SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
# Attributes
|
||||
ATTR_ON = "on"
|
||||
|
||||
# Services
|
||||
SERVICE_IDENTIFY = "identify"
|
||||
|
||||
@@ -15,9 +15,13 @@ from homeassistant.components.light import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
)
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from .const import SERVICE_IDENTIFY
|
||||
from .coordinator import ElgatoConfigEntry, ElgatoDataUpdateCoordinator
|
||||
from .entity import ElgatoEntity
|
||||
|
||||
@@ -33,6 +37,13 @@ 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."""
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""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",
|
||||
)
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260128.4"]
|
||||
"requirements": ["home-assistant-frontend==20260128.3"]
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def battery_level(self) -> int | None:
|
||||
def battery_level(self):
|
||||
"""Return battery value of the device."""
|
||||
return self._battery
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhik"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyHik==0.4.2"]
|
||||
"requirements": ["pyHik==0.4.1"]
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ 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(
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
"""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,
|
||||
config_validation as cv,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||
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
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
@@ -34,12 +30,6 @@ 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 = (
|
||||
|
||||
@@ -142,6 +142,3 @@ ERROR_KEYS = [
|
||||
"wrong_pin_code",
|
||||
"zone_generator_problem",
|
||||
]
|
||||
|
||||
MOW = "mow"
|
||||
PARK = "park"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""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,
|
||||
@@ -12,13 +14,16 @@ 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, MOW, PARK
|
||||
from .const import DOMAIN, ERROR_STATES
|
||||
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)
|
||||
@@ -36,6 +41,9 @@ SUPPORT_STATE_SERVICES = (
|
||||
| LawnMowerEntityFeature.PAUSE
|
||||
| LawnMowerEntityFeature.START_MOWING
|
||||
)
|
||||
MOW = "mow"
|
||||
PARK = "park"
|
||||
OVERRIDE_MODES = [MOW, PARK]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -54,6 +62,31 @@ 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):
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
"""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",
|
||||
)
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import LiebherrConfigEntry, LiebherrCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR]
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LiebherrConfigEntry) -> bool:
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
"""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()
|
||||
@@ -55,16 +55,23 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Liebherr sensor entities."""
|
||||
coordinators = entry.runtime_data
|
||||
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
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
class LiebherrSensor(LiebherrZoneEntity, SensorEntity):
|
||||
@@ -101,9 +108,9 @@ class LiebherrSensor(LiebherrZoneEntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the current value."""
|
||||
# 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)
|
||||
if (temp_control := self.temperature_control) is None:
|
||||
return None
|
||||
return self.entity_description.value_fn(temp_control)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -33,20 +33,6 @@
|
||||
}
|
||||
},
|
||||
"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"
|
||||
@@ -58,10 +44,5 @@
|
||||
"name": "Top zone"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"set_temperature_failed": {
|
||||
"message": "Failed to set temperature"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,11 @@ 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,
|
||||
@@ -27,12 +23,6 @@ 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)
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
"""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",
|
||||
)
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from pylitterbot import LitterRobot
|
||||
from pylitterbot.enums import LitterBoxStatus
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.vacuum import (
|
||||
StateVacuumEntity,
|
||||
@@ -15,12 +16,15 @@ 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,
|
||||
@@ -53,6 +57,16 @@ 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."""
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from speak2mary import MaryTTS
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -11,7 +9,6 @@ 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
|
||||
@@ -69,28 +66,26 @@ class MaryTTSProvider(Provider):
|
||||
self.name = "MaryTTS"
|
||||
|
||||
@property
|
||||
def default_language(self) -> str:
|
||||
def default_language(self):
|
||||
"""Return the default language."""
|
||||
return self._mary.locale
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
def supported_languages(self):
|
||||
"""Return list of supported languages."""
|
||||
return SUPPORT_LANGUAGES
|
||||
|
||||
@property
|
||||
def default_options(self) -> dict[str, Any]:
|
||||
def default_options(self):
|
||||
"""Return dict include default options."""
|
||||
return {CONF_EFFECT: self._effects}
|
||||
|
||||
@property
|
||||
def supported_options(self) -> list[str]:
|
||||
def supported_options(self):
|
||||
"""Return a list of supported options."""
|
||||
return SUPPORT_OPTIONS
|
||||
|
||||
def get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
def get_tts_audio(self, message, language, options):
|
||||
"""Load TTS from MaryTTS."""
|
||||
effects = options[CONF_EFFECT]
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
featuremap_contains=(clusters.Thermostat.Bitmaps.Feature.kSetback),
|
||||
),
|
||||
# Eve temperature offset with higher min/max
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
@@ -303,7 +304,27 @@ DISCOVERY_SCHEMAS = [
|
||||
required_attributes=(
|
||||
clusters.Thermostat.Attributes.LocalTemperatureCalibration,
|
||||
),
|
||||
vendor_id=(4874,),
|
||||
vendor_id=(4874,), # Eve Systems
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
key="TemperatureOffset",
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="temperature_offset",
|
||||
native_max_value=25, # Matter 1.3 limit
|
||||
native_min_value=-25, # Matter 1.3 limit
|
||||
native_step=0.5,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
required_attributes=(
|
||||
clusters.Thermostat.Attributes.LocalTemperatureCalibration,
|
||||
),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""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
|
||||
@@ -11,7 +10,6 @@ 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
|
||||
@@ -91,28 +89,26 @@ class MicrosoftProvider(Provider):
|
||||
self.name = "Microsoft"
|
||||
|
||||
@property
|
||||
def default_language(self) -> str:
|
||||
def default_language(self):
|
||||
"""Return the default language."""
|
||||
return self._lang
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
def supported_languages(self):
|
||||
"""Return list of supported languages."""
|
||||
return list(SUPPORTED_LANGUAGES)
|
||||
return SUPPORTED_LANGUAGES
|
||||
|
||||
@property
|
||||
def supported_options(self) -> list[str]:
|
||||
def supported_options(self):
|
||||
"""Return list of supported options like voice, emotion."""
|
||||
return [CONF_GENDER, CONF_TYPE]
|
||||
|
||||
@property
|
||||
def default_options(self) -> dict[str, Any]:
|
||||
def default_options(self):
|
||||
"""Return a dict include default options."""
|
||||
return {CONF_GENDER: self._gender, CONF_TYPE: self._type}
|
||||
|
||||
def get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
def get_tts_audio(self, message, language, options):
|
||||
"""Load TTS from Microsoft."""
|
||||
if language is None:
|
||||
language = self._lang
|
||||
|
||||
@@ -5,7 +5,6 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -13,7 +12,6 @@ from homeassistant.components.tts import (
|
||||
CONF_LANG,
|
||||
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
|
||||
Provider,
|
||||
TtsAudioType,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -44,18 +42,16 @@ class PicoProvider(Provider):
|
||||
self.name = "PicoTTS"
|
||||
|
||||
@property
|
||||
def default_language(self) -> str:
|
||||
def default_language(self):
|
||||
"""Return the default language."""
|
||||
return self._lang
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
def supported_languages(self):
|
||||
"""Return list of supported languages."""
|
||||
return SUPPORT_LANGUAGES
|
||||
|
||||
def get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
def get_tts_audio(self, message, language, options):
|
||||
"""Load TTS using pico2wave."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpf:
|
||||
fname = tmpf.name
|
||||
|
||||
@@ -18,26 +18,19 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers import 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, DOMAIN
|
||||
from .const import CONF_SERIAL_NUMBER
|
||||
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,
|
||||
@@ -47,6 +40,9 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
|
||||
DOMAIN = "rainbird"
|
||||
|
||||
|
||||
def _async_register_clientsession_shutdown(
|
||||
hass: HomeAssistant,
|
||||
entry: RainbirdConfigEntry,
|
||||
@@ -65,12 +61,6 @@ 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."""
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
"""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",
|
||||
)
|
||||
@@ -6,12 +6,15 @@ 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
|
||||
@@ -20,6 +23,12 @@ 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,
|
||||
@@ -38,6 +47,13 @@ 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."""
|
||||
|
||||
@@ -7,17 +7,22 @@ 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.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
)
|
||||
|
||||
from .const import SUPPORT_PTZ_SPEED
|
||||
from .entity import (
|
||||
ReolinkChannelCoordinatorEntity,
|
||||
ReolinkChannelEntityDescription,
|
||||
@@ -27,6 +32,9 @@ 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)
|
||||
@@ -170,6 +178,14 @@ 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."""
|
||||
@@ -203,8 +219,9 @@ class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity):
|
||||
await self.entity_description.method(self._host.api, self._channel)
|
||||
|
||||
@raise_translated_error
|
||||
async def async_ptz_move(self, *, speed: int) -> None:
|
||||
async def async_ptz_move(self, **kwargs: Any) -> 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
|
||||
)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Constants for the Reolink Camera integration."""
|
||||
|
||||
from homeassistant.components.camera import CameraEntityFeature
|
||||
|
||||
DOMAIN = "reolink"
|
||||
|
||||
CONF_USE_HTTPS = "use_https"
|
||||
@@ -15,5 +13,3 @@ 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
|
||||
|
||||
@@ -6,24 +6,17 @@ 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 (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
service,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import DOMAIN, SUPPORT_PTZ_SPEED
|
||||
from .const import DOMAIN
|
||||
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
|
||||
@@ -83,12 +76,3 @@ 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],
|
||||
)
|
||||
|
||||
@@ -23,9 +23,8 @@ 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 config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers import 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,
|
||||
@@ -48,20 +47,12 @@ 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)
|
||||
|
||||
@@ -52,10 +52,12 @@ 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)
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
"""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,
|
||||
)
|
||||
@@ -6,17 +6,24 @@ 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
|
||||
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
||||
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
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
GET_MAPS_SERVICE_NAME,
|
||||
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
|
||||
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
|
||||
)
|
||||
from .coordinator import (
|
||||
RoborockB01Q7UpdateCoordinator,
|
||||
RoborockConfigEntry,
|
||||
@@ -85,6 +92,33 @@ 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):
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -81,12 +81,6 @@ 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."""
|
||||
|
||||
@@ -41,7 +41,7 @@ from .coordinator import (
|
||||
TeslemetryEnergySiteLiveCoordinator,
|
||||
TeslemetryVehicleDataCoordinator,
|
||||
)
|
||||
from .helpers import async_update_device_sw_version, flatten
|
||||
from .helpers import flatten
|
||||
from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData
|
||||
from .services import async_setup_services
|
||||
|
||||
@@ -161,16 +161,13 @@ 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))
|
||||
|
||||
@@ -188,6 +185,7 @@ 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)
|
||||
|
||||
@@ -278,20 +276,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
|
||||
),
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Setup energy devices with models, versions, and listeners
|
||||
# Add energy device models
|
||||
for energysite in energysites:
|
||||
async_setup_energy_device(hass, entry, energysite, device_registry)
|
||||
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))
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# Remove devices that are no longer present
|
||||
for device_entry in dr.async_entries_for_config_entry(
|
||||
@@ -368,40 +369,6 @@ 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
|
||||
):
|
||||
@@ -413,54 +380,3 @@ 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
|
||||
|
||||
@@ -4,9 +4,7 @@ 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
|
||||
|
||||
@@ -70,14 +68,3 @@ 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)
|
||||
|
||||
@@ -46,7 +46,11 @@ rules:
|
||||
test fixture. Clarify _alt and _noscope fixture purposes. Test error messages in
|
||||
test_service_validation_errors.
|
||||
# Gold
|
||||
devices: done
|
||||
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.
|
||||
diagnostics: done
|
||||
discovery:
|
||||
status: exempt
|
||||
|
||||
@@ -1532,6 +1532,10 @@ 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(
|
||||
|
||||
@@ -1032,6 +1032,9 @@
|
||||
"vehicle_state_tpms_pressure_rr": {
|
||||
"name": "Tire pressure rear right"
|
||||
},
|
||||
"version": {
|
||||
"name": "Version"
|
||||
},
|
||||
"vin": {
|
||||
"name": "[%key:component::teslemetry::common::vehicle%]",
|
||||
"state": {
|
||||
|
||||
@@ -132,7 +132,7 @@ class TraccarEntity(TrackerEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def battery_level(self) -> int | None:
|
||||
def battery_level(self):
|
||||
"""Return battery value of the device."""
|
||||
return self._battery
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import asyncio
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
@@ -12,7 +11,6 @@ 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
|
||||
@@ -184,21 +182,17 @@ class VoiceRSSProvider(Provider):
|
||||
}
|
||||
|
||||
@property
|
||||
def default_language(self) -> str:
|
||||
def default_language(self):
|
||||
"""Return the default language."""
|
||||
return self._lang
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
def supported_languages(self):
|
||||
"""Return list of supported languages."""
|
||||
return SUPPORT_LANGUAGES
|
||||
|
||||
async def async_get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
async def async_get_tts_audio(self, message, language, options):
|
||||
"""Load TTS from VoiceRSS."""
|
||||
if TYPE_CHECKING:
|
||||
assert self.hass
|
||||
websession = async_get_clientsession(self.hass)
|
||||
form_data = self._form_data.copy()
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""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
|
||||
@@ -10,7 +9,6 @@ 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
|
||||
|
||||
@@ -165,28 +163,26 @@ class WatsonTTSProvider(Provider):
|
||||
self.name = "Watson TTS"
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
def supported_languages(self):
|
||||
"""Return a list of supported languages."""
|
||||
return self.supported_langs
|
||||
|
||||
@property
|
||||
def default_language(self) -> str:
|
||||
def default_language(self):
|
||||
"""Return the default language."""
|
||||
return self.default_lang
|
||||
|
||||
@property
|
||||
def default_options(self) -> dict[str, Any]:
|
||||
def default_options(self):
|
||||
"""Return dict include default options."""
|
||||
return {CONF_VOICE: self.default_voice}
|
||||
|
||||
@property
|
||||
def supported_options(self) -> list[str]:
|
||||
def supported_options(self):
|
||||
"""Return a list of supported options."""
|
||||
return [CONF_VOICE]
|
||||
|
||||
def get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
def get_tts_audio(self, message, language, options):
|
||||
"""Request TTS file from Watson TTS."""
|
||||
response = self.service.synthesize(
|
||||
text=message, accept=self.output_format, voice=options[CONF_VOICE]
|
||||
|
||||
@@ -23,7 +23,6 @@ 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)
|
||||
|
||||
@@ -32,8 +31,6 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -12,12 +12,17 @@ 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 = (
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 (
|
||||
@@ -21,21 +22,27 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerState,
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES
|
||||
from homeassistant.core import HomeAssistant, ServiceResponse
|
||||
from homeassistant.const import ATTR_COMMAND, ATTR_SUPPORTED_FEATURES
|
||||
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
||||
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
|
||||
@@ -63,6 +70,34 @@ 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,
|
||||
@@ -70,6 +105,12 @@ 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)])
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""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,
|
||||
)
|
||||
@@ -3,7 +3,6 @@
|
||||
import asyncio
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
@@ -12,7 +11,6 @@ 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
|
||||
@@ -106,26 +104,22 @@ class YandexSpeechKitProvider(Provider):
|
||||
self.name = "YandexTTS"
|
||||
|
||||
@property
|
||||
def default_language(self) -> str:
|
||||
def default_language(self):
|
||||
"""Return the default language."""
|
||||
return self._language
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
def supported_languages(self):
|
||||
"""Return list of supported languages."""
|
||||
return SUPPORT_LANGUAGES
|
||||
|
||||
@property
|
||||
def supported_options(self) -> list[str]:
|
||||
def supported_options(self):
|
||||
"""Return list of supported options."""
|
||||
return SUPPORTED_OPTIONS
|
||||
|
||||
async def async_get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
async def async_get_tts_audio(self, message, language, options):
|
||||
"""Load TTS from yandex."""
|
||||
if TYPE_CHECKING:
|
||||
assert self.hass
|
||||
websession = async_get_clientsession(self.hass)
|
||||
actual_language = language
|
||||
|
||||
|
||||
2
homeassistant/generated/labs.py
generated
2
homeassistant/generated/labs.py
generated
@@ -7,7 +7,7 @@ LABS_PREVIEW_FEATURES = {
|
||||
"analytics": {
|
||||
"snapshots": {
|
||||
"feedback_url": "https://forms.gle/GqvRmgmghSDco8M46",
|
||||
"learn_more_url": "https://www.home-assistant.io/blog/2026/02/02/about-device-database/",
|
||||
"learn_more_url": "",
|
||||
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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.4
|
||||
home-assistant-frontend==20260128.3
|
||||
home-assistant-intents==2026.1.28
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
@@ -1484,7 +1484,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = {
|
||||
TypeHintMatch(
|
||||
function_name="battery_level",
|
||||
return_type=["int", None],
|
||||
mandatory=True,
|
||||
),
|
||||
TypeHintMatch(
|
||||
function_name="source_type",
|
||||
@@ -1509,22 +1508,18 @@ _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,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1534,17 +1529,14 @@ _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",
|
||||
@@ -2710,29 +2702,24 @@ _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
4
requirements_all.txt
generated
@@ -1219,7 +1219,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260128.4
|
||||
home-assistant-frontend==20260128.3
|
||||
|
||||
# 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.2
|
||||
pyHik==0.4.1
|
||||
|
||||
# homeassistant.components.homee
|
||||
pyHomee==1.3.8
|
||||
|
||||
4
requirements_test_all.txt
generated
4
requirements_test_all.txt
generated
@@ -1077,7 +1077,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260128.4
|
||||
home-assistant-frontend==20260128.3
|
||||
|
||||
# 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.2
|
||||
pyHik==0.4.1
|
||||
|
||||
# homeassistant.components.homee
|
||||
pyHomee==1.3.8
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.util.yaml import load_yaml_dict
|
||||
from .model import Config, Integration, ScaledQualityScaleTiers
|
||||
from .quality_scale_validation import (
|
||||
RuleValidationProtocol,
|
||||
action_setup,
|
||||
config_entry_unloading,
|
||||
config_flow,
|
||||
diagnostics,
|
||||
@@ -42,7 +41,7 @@ class Rule:
|
||||
|
||||
ALL_RULES = [
|
||||
# BRONZE
|
||||
Rule("action-setup", ScaledQualityScaleTiers.BRONZE, action_setup),
|
||||
Rule("action-setup", ScaledQualityScaleTiers.BRONZE),
|
||||
Rule("appropriate-polling", ScaledQualityScaleTiers.BRONZE),
|
||||
Rule("brands", ScaledQualityScaleTiers.BRONZE),
|
||||
Rule("common-modules", ScaledQualityScaleTiers.BRONZE),
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
"""Enforce that the integration service actions are registered in async_setup.
|
||||
|
||||
https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/action-setup/
|
||||
"""
|
||||
|
||||
import ast
|
||||
|
||||
from script.hassfest import ast_parse_module
|
||||
from script.hassfest.manifest import Platform
|
||||
from script.hassfest.model import Config, Integration
|
||||
|
||||
|
||||
def _get_setup_entry_function(module: ast.Module) -> ast.AsyncFunctionDef | None:
|
||||
"""Get async_setup_entry function."""
|
||||
for item in module.body:
|
||||
if isinstance(item, ast.AsyncFunctionDef) and item.name == "async_setup_entry":
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def _calls_service_registration(
|
||||
async_setup_entry_function: ast.AsyncFunctionDef,
|
||||
) -> bool:
|
||||
"""Check if there are calls to service registration."""
|
||||
for node in ast.walk(async_setup_entry_function):
|
||||
if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)):
|
||||
continue
|
||||
|
||||
if node.func.attr == "async_register_entity_service":
|
||||
return True
|
||||
|
||||
if (
|
||||
isinstance(node.func.value, ast.Attribute)
|
||||
and isinstance(node.func.value.value, ast.Name)
|
||||
and node.func.value.value.id == "hass"
|
||||
and node.func.value.attr == "services"
|
||||
and node.func.attr in {"async_register", "register"}
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def validate(
|
||||
config: Config, integration: Integration, *, rules_done: set[str]
|
||||
) -> list[str] | None:
|
||||
"""Validate that service actions are registered in async_setup."""
|
||||
|
||||
errors = []
|
||||
|
||||
module_file = integration.path / "__init__.py"
|
||||
module = ast_parse_module(module_file)
|
||||
if (
|
||||
async_setup_entry := _get_setup_entry_function(module)
|
||||
) and _calls_service_registration(async_setup_entry):
|
||||
errors.append(
|
||||
f"Integration registers services in {module_file} (async_setup_entry)"
|
||||
)
|
||||
|
||||
for platform in Platform:
|
||||
module_file = integration.path / f"{platform}.py"
|
||||
if not module_file.exists():
|
||||
continue
|
||||
module = ast_parse_module(module_file)
|
||||
|
||||
if (
|
||||
async_setup_entry := _get_setup_entry_function(module)
|
||||
) and _calls_service_registration(async_setup_entry):
|
||||
errors.append(
|
||||
f"Integration registers services in {module_file} (async_setup_entry)"
|
||||
)
|
||||
|
||||
return errors
|
||||
@@ -10,9 +10,11 @@ import pytest
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components import fan
|
||||
from homeassistant.components.bond.const import DOMAIN
|
||||
from homeassistant.components.bond.const import (
|
||||
DOMAIN,
|
||||
SERVICE_SET_FAN_SPEED_TRACKED_STATE,
|
||||
)
|
||||
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,
|
||||
|
||||
@@ -5,11 +5,13 @@ from datetime import timedelta
|
||||
from bond_async import Action, DeviceType
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bond.const import DOMAIN
|
||||
from homeassistant.components.bond.services import (
|
||||
from homeassistant.components.bond.const 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,
|
||||
|
||||
@@ -5,9 +5,9 @@ from datetime import timedelta
|
||||
from bond_async import Action, DeviceType
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bond.const import DOMAIN
|
||||
from homeassistant.components.bond.services import (
|
||||
from homeassistant.components.bond.const import (
|
||||
ATTR_POWER_STATE,
|
||||
DOMAIN,
|
||||
SERVICE_SET_POWER_TRACKED_STATE,
|
||||
)
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
|
||||
@@ -6,8 +6,7 @@ from elgato import ElgatoError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.elgato.const import DOMAIN
|
||||
from homeassistant.components.elgato.services import SERVICE_IDENTIFY
|
||||
from homeassistant.components.elgato.const import DOMAIN, SERVICE_IDENTIFY
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
|
||||
@@ -36,9 +36,6 @@ MOCK_DEVICE_STATE = DeviceState(
|
||||
name="Fridge",
|
||||
type="fridge",
|
||||
value=5,
|
||||
target=4,
|
||||
min=2,
|
||||
max=8,
|
||||
unit=TemperatureUnit.CELSIUS,
|
||||
),
|
||||
TemperatureControl(
|
||||
@@ -47,9 +44,6 @@ MOCK_DEVICE_STATE = DeviceState(
|
||||
name="Freezer",
|
||||
type="freezer",
|
||||
value=-18,
|
||||
target=-18,
|
||||
min=-24,
|
||||
max=-16,
|
||||
unit=TemperatureUnit.CELSIUS,
|
||||
),
|
||||
],
|
||||
@@ -106,13 +100,11 @@ 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)
|
||||
|
||||
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 hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
# 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',
|
||||
})
|
||||
# ---
|
||||
@@ -1,321 +0,0 @@
|
||||
"""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
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Test the Liebherr sensor platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
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, Platform
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@@ -49,7 +49,6 @@ 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(
|
||||
@@ -74,9 +73,8 @@ async def test_single_zone_sensor(
|
||||
)
|
||||
|
||||
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 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)
|
||||
|
||||
@@ -250,3 +248,9 @@ 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
|
||||
|
||||
@@ -8,7 +8,7 @@ from unittest.mock import MagicMock
|
||||
from pylitterbot import Robot
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.litterrobot.services import SERVICE_SET_SLEEP_MODE
|
||||
from homeassistant.components.litterrobot.vacuum import SERVICE_SET_SLEEP_MODE
|
||||
from homeassistant.components.vacuum import (
|
||||
DOMAIN as VACUUM_DOMAIN,
|
||||
SERVICE_START,
|
||||
|
||||
@@ -176,6 +176,66 @@
|
||||
'state': '0.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[aqara_thermostat_w500][number.floor_heating_thermostat_temperature_offset-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 25,
|
||||
'min': -25,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 0.5,
|
||||
}),
|
||||
'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.floor_heating_thermostat_temperature_offset',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Temperature offset',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Temperature offset',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature_offset',
|
||||
'unique_id': '00000000000004D2-0000000000000064-MatterNodeDevice-1-TemperatureOffset-513-16',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[aqara_thermostat_w500][number.floor_heating_thermostat_temperature_offset-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Floor Heating Thermostat Temperature offset',
|
||||
'max': 25,
|
||||
'min': -25,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 0.5,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.floor_heating_thermostat_temperature_offset',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[aqara_u200][number.aqara_smart_lock_u200_user_code_temporary_disable_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -3463,6 +3523,66 @@
|
||||
'state': '47',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[mock_thermostat][number.mock_thermostat_temperature_offset-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 25,
|
||||
'min': -25,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 0.5,
|
||||
}),
|
||||
'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.mock_thermostat_temperature_offset',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Temperature offset',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Temperature offset',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature_offset',
|
||||
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-TemperatureOffset-513-16',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[mock_thermostat][number.mock_thermostat_temperature_offset-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Mock Thermostat Temperature offset',
|
||||
'max': 25,
|
||||
'min': -25,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 0.5,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.mock_thermostat_temperature_offset',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[mock_valve][number.mock_valve_default_open_duration-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.services import (
|
||||
from homeassistant.components.roborock.const import (
|
||||
GET_MAPS_SERVICE_NAME,
|
||||
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
|
||||
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': '123456',
|
||||
'sw_version': '23.44.0 eb113390',
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
@@ -52,12 +52,12 @@
|
||||
}),
|
||||
'manufacturer': 'Tesla',
|
||||
'model': 'Model 3',
|
||||
'model_id': '3',
|
||||
'model_id': None,
|
||||
'name': 'Test',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': 'LRW3F7EK4NC700000',
|
||||
'sw_version': '2026.0.0',
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -2362,6 +2362,68 @@
|
||||
'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({
|
||||
|
||||
@@ -21,7 +21,6 @@ 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,
|
||||
)
|
||||
@@ -43,9 +42,7 @@ from .const import (
|
||||
ENERGY_HISTORY,
|
||||
LIVE_STATUS,
|
||||
PRODUCTS_MODERN,
|
||||
SITE_INFO,
|
||||
UNIQUE_ID,
|
||||
VEHICLE_DATA,
|
||||
VEHICLE_DATA_ALT,
|
||||
)
|
||||
|
||||
@@ -647,136 +644,3 @@ 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"
|
||||
|
||||
@@ -26,22 +26,20 @@ from homeassistant.components.media_player import (
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.components.webostv.const import (
|
||||
ATTR_BUTTON,
|
||||
ATTR_PAYLOAD,
|
||||
ATTR_SOUND_OUTPUT,
|
||||
DOMAIN,
|
||||
LIVE_TV_APP_ID,
|
||||
SERVICE_BUTTON,
|
||||
SERVICE_COMMAND,
|
||||
SERVICE_SELECT_SOUND_OUTPUT,
|
||||
WebOsTvCommandError,
|
||||
)
|
||||
from homeassistant.components.webostv.media_player import (
|
||||
SUPPORT_WEBOSTV,
|
||||
SUPPORT_WEBOSTV_VOLUME,
|
||||
)
|
||||
from homeassistant.components.webostv.services import (
|
||||
ATTR_BUTTON,
|
||||
SERVICE_BUTTON,
|
||||
SERVICE_COMMAND,
|
||||
SERVICE_SELECT_SOUND_OUTPUT,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_COMMAND,
|
||||
|
||||
Reference in New Issue
Block a user