mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
2024.9.1 (#125420)
This commit is contained in:
commit
444560543c
@ -262,7 +262,7 @@ class Airtouch5AC(Airtouch5ClimateEntity):
|
||||
_LOGGER.debug("Argument `temperature` is missing in set_temperature")
|
||||
return
|
||||
|
||||
await self._control(temp=temp)
|
||||
await self._control(setpoint=SetpointControl.CHANGE_SETPOINT, temp=temp)
|
||||
|
||||
|
||||
class Airtouch5Zone(Airtouch5ClimateEntity):
|
||||
|
@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyatv", "srptools"],
|
||||
"requirements": ["pyatv==0.15.0"],
|
||||
"requirements": ["pyatv==0.15.1"],
|
||||
"zeroconf": [
|
||||
"_mediaremotetv._tcp.local.",
|
||||
"_companion-link._tcp.local.",
|
||||
|
@ -36,10 +36,11 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
try:
|
||||
max_power = (await self.api.get_device_info()).maxPower
|
||||
device_info = await self.api.get_device_info()
|
||||
except (ConnectionError, TimeoutError):
|
||||
raise UpdateFailed from None
|
||||
self.api.max_power = max_power
|
||||
self.api.max_power = device_info.maxPower
|
||||
self.api.min_power = device_info.minPower
|
||||
|
||||
async def _async_update_data(self) -> ApSystemsSensorData:
|
||||
output_data = await self.api.get_output_data()
|
||||
|
@ -26,7 +26,6 @@ async def async_setup_entry(
|
||||
class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
|
||||
"""Base sensor to be used with description."""
|
||||
|
||||
_attr_native_min_value = 30
|
||||
_attr_native_step = 1
|
||||
_attr_device_class = NumberDeviceClass.POWER
|
||||
_attr_mode = NumberMode.BOX
|
||||
@ -42,6 +41,7 @@ class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
|
||||
self._api = data.coordinator.api
|
||||
self._attr_unique_id = f"{data.device_id}_output_limit"
|
||||
self._attr_native_max_value = data.coordinator.api.max_power
|
||||
self._attr_native_min_value = data.coordinator.api.min_power
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Set the state with the value fetched from the inverter."""
|
||||
|
@ -56,7 +56,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
refresh_token = await api.authenticate(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||
)
|
||||
except ApiException:
|
||||
except (ApiException, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except AuthenticationFailed:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
@ -56,7 +56,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
|
||||
so entities can quickly look up their data.
|
||||
"""
|
||||
|
||||
async with asyncio.timeout(10):
|
||||
async with asyncio.timeout(30):
|
||||
# Check if the refresh token is expired
|
||||
expiry_time = (
|
||||
self.refresh_token_creation_time
|
||||
@ -72,7 +72,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
|
||||
softeners = await self.aquacell_api.get_all_softeners()
|
||||
except AuthenticationFailed as err:
|
||||
raise ConfigEntryError from err
|
||||
except AquacellApiException as err:
|
||||
except (AquacellApiException, TimeoutError) as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
return {softener.dsn: softener for softener in softeners}
|
||||
|
@ -7,6 +7,9 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
||||
from homeassistant.components.device_automation.exceptions import (
|
||||
InvalidDeviceAutomationConfig,
|
||||
)
|
||||
from homeassistant.components.homeassistant.triggers import event as event_trigger
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
@ -43,33 +46,46 @@ TRIGGERS_BY_EVENT_CLASS = {
|
||||
EVENT_CLASS_DIMMER: {"rotate_left", "rotate_right"},
|
||||
}
|
||||
|
||||
SCHEMA_BY_EVENT_CLASS = {
|
||||
EVENT_CLASS_BUTTON: DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_BUTTON]),
|
||||
vol.Required(CONF_SUBTYPE): vol.In(
|
||||
TRIGGERS_BY_EVENT_CLASS[EVENT_CLASS_BUTTON]
|
||||
),
|
||||
}
|
||||
),
|
||||
EVENT_CLASS_DIMMER: DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_DIMMER]),
|
||||
vol.Required(CONF_SUBTYPE): vol.In(
|
||||
TRIGGERS_BY_EVENT_CLASS[EVENT_CLASS_DIMMER]
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
{vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str}
|
||||
)
|
||||
|
||||
|
||||
async def async_validate_trigger_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate trigger config."""
|
||||
return SCHEMA_BY_EVENT_CLASS.get(config[CONF_TYPE], DEVICE_TRIGGER_BASE_SCHEMA)( # type: ignore[no-any-return]
|
||||
config
|
||||
config = TRIGGER_SCHEMA(config)
|
||||
event_class = config[CONF_TYPE]
|
||||
event_type = config[CONF_SUBTYPE]
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get(config[CONF_DEVICE_ID])
|
||||
assert device is not None
|
||||
config_entries = [
|
||||
hass.config_entries.async_get_entry(entry_id)
|
||||
for entry_id in device.config_entries
|
||||
]
|
||||
bthome_config_entry = next(
|
||||
iter(entry for entry in config_entries if entry and entry.domain == DOMAIN)
|
||||
)
|
||||
event_classes: list[str] = bthome_config_entry.data.get(
|
||||
CONF_DISCOVERED_EVENT_CLASSES, []
|
||||
)
|
||||
|
||||
if event_class not in event_classes:
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"BTHome trigger {event_class} is not valid for device "
|
||||
f"{device} ({config[CONF_DEVICE_ID]})"
|
||||
)
|
||||
|
||||
if event_type not in TRIGGERS_BY_EVENT_CLASS.get(event_class.split("_")[0], ()):
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"BTHome trigger {event_type} is not valid for device "
|
||||
f"{device} ({config[CONF_DEVICE_ID]})"
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
async def async_get_triggers(
|
||||
|
@ -119,7 +119,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
self._partition_number = partition_number
|
||||
self._panic_type = panic_type
|
||||
self._alarm_control_panel_option_default_code = code
|
||||
self._attr_code_format = CodeFormat.NUMBER
|
||||
self._attr_code_format = CodeFormat.NUMBER if not code else None
|
||||
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
super().__init__(alarm_name, info, controller)
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240904.0"]
|
||||
"requirements": ["home-assistant-frontend==20240906.0"]
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]):
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="FYTA Coordinator",
|
||||
update_interval=timedelta(seconds=60),
|
||||
update_interval=timedelta(minutes=4),
|
||||
)
|
||||
self.fyta = fyta
|
||||
|
||||
|
@ -226,7 +226,11 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
effect = effect_str = kwargs.get(ATTR_EFFECT)
|
||||
if effect_str in (EFFECT_NONE, EFFECT_NONE.lower()):
|
||||
effect = EffectStatus.NO_EFFECT
|
||||
# ignore effect if set to "None" and we have no effect active
|
||||
# the special effect "None" is only used to stop an active effect
|
||||
# but sending it while no effect is active can actually result in issues
|
||||
# https://github.com/home-assistant/core/issues/122165
|
||||
effect = None if self.effect == EFFECT_NONE else EffectStatus.NO_EFFECT
|
||||
elif effect_str is not None:
|
||||
# work out if we got a regular effect or timed effect
|
||||
effect = EffectStatus(effect_str)
|
||||
|
@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pypck"],
|
||||
"requirements": ["pypck==0.7.21", "lcn-frontend==0.1.6"]
|
||||
"requirements": ["pypck==0.7.22", "lcn-frontend==0.1.6"]
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ from homeassistant.components.media_player import (
|
||||
MediaType,
|
||||
RepeatMode,
|
||||
)
|
||||
from homeassistant.components.media_player.browse_media import (
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@ -59,6 +62,7 @@ SOURCE_MAP: dict[PlayingMode, str] = {
|
||||
PlayingMode.FM: "FM Radio",
|
||||
PlayingMode.RCA: "RCA",
|
||||
PlayingMode.UDISK: "USB",
|
||||
PlayingMode.FOLLOWER: "Follower",
|
||||
}
|
||||
|
||||
SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k for k, v in SOURCE_MAP.items()}
|
||||
@ -233,10 +237,14 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity):
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
) -> None:
|
||||
"""Play a piece of media."""
|
||||
media = await media_source.async_resolve_media(
|
||||
if media_source.is_media_source_id(media_id):
|
||||
play_item = await media_source.async_resolve_media(
|
||||
self.hass, media_id, self.entity_id
|
||||
)
|
||||
await self._bridge.player.play(media.url)
|
||||
media_id = play_item.url
|
||||
|
||||
url = async_process_play_media_url(self.hass, media_id)
|
||||
await self._bridge.player.play(url)
|
||||
|
||||
def _update_properties(self) -> None:
|
||||
"""Update the properties of the media player."""
|
||||
|
@ -358,8 +358,8 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
device,
|
||||
coolSetpoint=target_temp_high,
|
||||
heatSetpoint=target_temp_low,
|
||||
cool_setpoint=target_temp_high,
|
||||
heat_setpoint=target_temp_low,
|
||||
mode=mode,
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
@ -371,11 +371,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
try:
|
||||
if self.hvac_mode == HVACMode.COOL:
|
||||
await self._update_thermostat(
|
||||
self.location, device, coolSetpoint=temp
|
||||
self.location, device, cool_setpoint=temp
|
||||
)
|
||||
else:
|
||||
await self._update_thermostat(
|
||||
self.location, device, heatSetpoint=temp
|
||||
self.location, device, heat_setpoint=temp
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
@ -410,7 +410,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
self.location,
|
||||
self.device,
|
||||
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
autoChangeoverActive=False,
|
||||
auto_changeover_active=False,
|
||||
)
|
||||
# Sleep 3 seconds before proceeding
|
||||
await asyncio.sleep(3)
|
||||
@ -422,7 +422,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
self.location,
|
||||
self.device,
|
||||
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
autoChangeoverActive=True,
|
||||
auto_changeover_active=True,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
@ -430,7 +430,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
HVAC_MODES[self.device.changeable_values.mode],
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location, self.device, autoChangeoverActive=True
|
||||
self.location, self.device, auto_changeover_active=True
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode])
|
||||
@ -438,13 +438,13 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
self.location,
|
||||
self.device,
|
||||
mode=LYRIC_HVAC_MODES[hvac_mode],
|
||||
autoChangeoverActive=False,
|
||||
auto_changeover_active=False,
|
||||
)
|
||||
|
||||
async def _async_set_hvac_mode_lcc(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set hvac mode for LCC devices (e.g., T5,6)."""
|
||||
_LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode])
|
||||
# Set autoChangeoverActive to True if the mode being passed is Auto
|
||||
# Set auto_changeover_active to True if the mode being passed is Auto
|
||||
# otherwise leave unchanged.
|
||||
if (
|
||||
LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL
|
||||
@ -458,7 +458,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
self.location,
|
||||
self.device,
|
||||
mode=LYRIC_HVAC_MODES[hvac_mode],
|
||||
autoChangeoverActive=auto_changeover,
|
||||
auto_changeover_active=auto_changeover,
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
@ -466,7 +466,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
_LOGGER.debug("Set preset mode: %s", preset_mode)
|
||||
try:
|
||||
await self._update_thermostat(
|
||||
self.location, self.device, thermostatSetpointStatus=preset_mode
|
||||
self.location, self.device, thermostat_setpoint_status=preset_mode
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
@ -479,8 +479,8 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
self.device,
|
||||
thermostatSetpointStatus=PRESET_HOLD_UNTIL,
|
||||
nextPeriodTime=time_period,
|
||||
thermostat_setpoint_status=PRESET_HOLD_UNTIL,
|
||||
next_period_time=time_period,
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
|
@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiorussound import Russound
|
||||
from aiorussound import RussoundClient, RussoundTcpConnectionHandler
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
@ -16,7 +16,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type RussoundConfigEntry = ConfigEntry[Russound]
|
||||
type RussoundConfigEntry = ConfigEntry[RussoundClient]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) -> bool:
|
||||
@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) ->
|
||||
|
||||
host = entry.data[CONF_HOST]
|
||||
port = entry.data[CONF_PORT]
|
||||
russ = Russound(hass.loop, host, port)
|
||||
russ = RussoundClient(RussoundTcpConnectionHandler(hass.loop, host, port))
|
||||
|
||||
@callback
|
||||
def is_connected_updated(connected: bool) -> None:
|
||||
@ -37,14 +37,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) ->
|
||||
port,
|
||||
)
|
||||
|
||||
russ.add_connection_callback(is_connected_updated)
|
||||
|
||||
russ.connection_handler.add_connection_callback(is_connected_updated)
|
||||
try:
|
||||
async with asyncio.timeout(CONNECT_TIMEOUT):
|
||||
await russ.connect()
|
||||
except RUSSOUND_RIO_EXCEPTIONS as err:
|
||||
raise ConfigEntryNotReady(f"Error while connecting to {host}:{port}") from err
|
||||
|
||||
entry.runtime_data = russ
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
@ -6,7 +6,7 @@ import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiorussound import Controller, Russound
|
||||
from aiorussound import Controller, RussoundClient, RussoundTcpConnectionHandler
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@ -54,8 +54,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
host = user_input[CONF_HOST]
|
||||
port = user_input[CONF_PORT]
|
||||
|
||||
controllers = None
|
||||
russ = Russound(self.hass.loop, host, port)
|
||||
russ = RussoundClient(
|
||||
RussoundTcpConnectionHandler(self.hass.loop, host, port)
|
||||
)
|
||||
try:
|
||||
async with asyncio.timeout(CONNECT_TIMEOUT):
|
||||
await russ.connect()
|
||||
@ -87,7 +88,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
port = import_data.get(CONF_PORT, 9621)
|
||||
|
||||
# Connection logic is repeated here since this method will be removed in future releases
|
||||
russ = Russound(self.hass.loop, host, port)
|
||||
russ = RussoundClient(RussoundTcpConnectionHandler(self.hass.loop, host, port))
|
||||
try:
|
||||
async with asyncio.timeout(CONNECT_TIMEOUT):
|
||||
await russ.connect()
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from aiorussound import CommandException
|
||||
from aiorussound import CommandError
|
||||
from aiorussound.const import FeatureFlag
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerEntityFeature
|
||||
@ -10,7 +10,7 @@ from homeassistant.components.media_player import MediaPlayerEntityFeature
|
||||
DOMAIN = "russound_rio"
|
||||
|
||||
RUSSOUND_RIO_EXCEPTIONS = (
|
||||
CommandException,
|
||||
CommandError,
|
||||
ConnectionRefusedError,
|
||||
TimeoutError,
|
||||
asyncio.CancelledError,
|
||||
|
@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aiorussound import Controller
|
||||
from aiorussound import Controller, RussoundTcpConnectionHandler
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@ -53,7 +53,6 @@ class RussoundBaseEntity(Entity):
|
||||
or f"{self._primary_mac_address}-{self._controller.controller_id}"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=f"http://{self._instance.host}",
|
||||
# Use MAC address of Russound device as identifier
|
||||
identifiers={(DOMAIN, self._device_identifier)},
|
||||
manufacturer="Russound",
|
||||
@ -61,6 +60,10 @@ class RussoundBaseEntity(Entity):
|
||||
model=controller.controller_type,
|
||||
sw_version=controller.firmware_version,
|
||||
)
|
||||
if isinstance(self._instance.connection_handler, RussoundTcpConnectionHandler):
|
||||
self._attr_device_info["configuration_url"] = (
|
||||
f"http://{self._instance.connection_handler.host}"
|
||||
)
|
||||
if controller.parent_controller:
|
||||
self._attr_device_info["via_device"] = (
|
||||
DOMAIN,
|
||||
@ -79,8 +82,12 @@ class RussoundBaseEntity(Entity):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
self._instance.add_connection_callback(self._is_connected_updated)
|
||||
self._instance.connection_handler.add_connection_callback(
|
||||
self._is_connected_updated
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove callbacks."""
|
||||
self._instance.remove_connection_callback(self._is_connected_updated)
|
||||
self._instance.connection_handler.remove_connection_callback(
|
||||
self._is_connected_updated
|
||||
)
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiorussound==2.3.2"]
|
||||
"requirements": ["aiorussound==3.0.4"]
|
||||
}
|
||||
|
@ -84,14 +84,16 @@ async def async_setup_entry(
|
||||
"""Set up the Russound RIO platform."""
|
||||
russ = entry.runtime_data
|
||||
|
||||
await russ.init_sources()
|
||||
sources = russ.sources
|
||||
for source in sources.values():
|
||||
await source.watch()
|
||||
|
||||
# Discover controllers
|
||||
controllers = await russ.enumerate_controllers()
|
||||
|
||||
entities = []
|
||||
for controller in controllers.values():
|
||||
sources = controller.sources
|
||||
for source in sources.values():
|
||||
await source.watch()
|
||||
for zone in controller.zones.values():
|
||||
await zone.watch()
|
||||
mp = RussoundZoneDevice(zone, sources)
|
||||
@ -154,7 +156,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the device."""
|
||||
status = self._zone.status
|
||||
status = self._zone.properties.status
|
||||
if status == "ON":
|
||||
return MediaPlayerState.ON
|
||||
if status == "OFF":
|
||||
@ -174,22 +176,22 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return self._current_source().song_name
|
||||
return self._current_source().properties.song_name
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media, music track only."""
|
||||
return self._current_source().artist_name
|
||||
return self._current_source().properties.artist_name
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Album name of current playing media, music track only."""
|
||||
return self._current_source().album_name
|
||||
return self._current_source().properties.album_name
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
return self._current_source().cover_art_url
|
||||
return self._current_source().properties.cover_art_url
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
@ -198,7 +200,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
||||
Value is returned based on a range (0..50).
|
||||
Therefore float divide by 50 to get to the required range.
|
||||
"""
|
||||
return float(self._zone.volume or "0") / 50.0
|
||||
return float(self._zone.properties.volume or "0") / 50.0
|
||||
|
||||
@command
|
||||
async def async_turn_off(self) -> None:
|
||||
@ -214,7 +216,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set the volume level."""
|
||||
rvol = int(volume * 50.0)
|
||||
await self._zone.set_volume(rvol)
|
||||
await self._zone.set_volume(str(rvol))
|
||||
|
||||
@command
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
|
@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sfrbox_api.bridge import SFRBox
|
||||
from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError
|
||||
@ -46,6 +47,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Preload system information
|
||||
await data.system.async_config_entry_first_refresh()
|
||||
system_info = data.system.data
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
|
||||
# Preload other coordinators (based on net infrastructure)
|
||||
tasks = [data.wan.async_config_entry_first_refresh()]
|
||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo
|
||||
|
||||
@ -65,19 +66,22 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the sensors."""
|
||||
data: DomainData = hass.data[DOMAIN][entry.entry_id]
|
||||
system_info = data.system.data
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
|
||||
entities: list[SFRBoxBinarySensor] = [
|
||||
SFRBoxBinarySensor(data.wan, description, data.system.data)
|
||||
SFRBoxBinarySensor(data.wan, description, system_info)
|
||||
for description in WAN_SENSOR_TYPES
|
||||
]
|
||||
if (net_infra := data.system.data.net_infra) == "adsl":
|
||||
if (net_infra := system_info.net_infra) == "adsl":
|
||||
entities.extend(
|
||||
SFRBoxBinarySensor(data.dsl, description, data.system.data)
|
||||
SFRBoxBinarySensor(data.dsl, description, system_info)
|
||||
for description in DSL_SENSOR_TYPES
|
||||
)
|
||||
elif net_infra == "ftth":
|
||||
entities.extend(
|
||||
SFRBoxBinarySensor(data.ftth, description, data.system.data)
|
||||
SFRBoxBinarySensor(data.ftth, description, system_info)
|
||||
for description in FTTH_SENSOR_TYPES
|
||||
)
|
||||
|
||||
@ -111,4 +115,6 @@ class SFRBoxBinarySensor[_T](
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the native value of the device."""
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
from typing import TYPE_CHECKING, Any, Concatenate
|
||||
|
||||
from sfrbox_api.bridge import SFRBox
|
||||
from sfrbox_api.exceptions import SFRBoxError
|
||||
@ -69,10 +69,12 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the buttons."""
|
||||
data: DomainData = hass.data[DOMAIN][entry.entry_id]
|
||||
system_info = data.system.data
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
|
||||
entities = [
|
||||
SFRBoxButton(data.box, description, data.system.data)
|
||||
for description in BUTTON_TYPES
|
||||
SFRBoxButton(data.box, description, system_info) for description in BUTTON_TYPES
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from sfrbox_api.bridge import SFRBox
|
||||
from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError
|
||||
@ -51,6 +51,8 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
except SFRBoxError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
await self.async_set_unique_id(system_info.mac_addr)
|
||||
self._abort_if_unique_id_configured()
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
|
@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
|
||||
class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT | None]):
|
||||
"""Coordinator to manage data updates."""
|
||||
|
||||
def __init__(
|
||||
@ -23,14 +23,14 @@ class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
hass: HomeAssistant,
|
||||
box: SFRBox,
|
||||
name: str,
|
||||
method: Callable[[SFRBox], Coroutine[Any, Any, _DataT]],
|
||||
method: Callable[[SFRBox], Coroutine[Any, Any, _DataT | None]],
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
self.box = box
|
||||
self._method = method
|
||||
super().__init__(hass, _LOGGER, name=name, update_interval=_SCAN_INTERVAL)
|
||||
|
||||
async def _async_update_data(self) -> _DataT:
|
||||
async def _async_update_data(self) -> _DataT | None:
|
||||
"""Update data."""
|
||||
try:
|
||||
return await self._method(self.box)
|
||||
|
@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -12,9 +12,18 @@ from homeassistant.core import HomeAssistant
|
||||
from .const import DOMAIN
|
||||
from .models import DomainData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import DataclassInstance
|
||||
|
||||
TO_REDACT = {"mac_addr", "serial_number", "ip_addr", "ipv6_addr"}
|
||||
|
||||
|
||||
def _async_redact_data(obj: DataclassInstance | None) -> dict[str, Any] | None:
|
||||
if obj is None:
|
||||
return None
|
||||
return async_redact_data(dataclasses.asdict(obj), TO_REDACT)
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
@ -27,21 +36,9 @@ async def async_get_config_entry_diagnostics(
|
||||
"data": dict(entry.data),
|
||||
},
|
||||
"data": {
|
||||
"dsl": async_redact_data(
|
||||
dataclasses.asdict(await data.system.box.dsl_get_info()),
|
||||
TO_REDACT,
|
||||
),
|
||||
"ftth": async_redact_data(
|
||||
dataclasses.asdict(await data.system.box.ftth_get_info()),
|
||||
TO_REDACT,
|
||||
),
|
||||
"system": async_redact_data(
|
||||
dataclasses.asdict(await data.system.box.system_get_info()),
|
||||
TO_REDACT,
|
||||
),
|
||||
"wan": async_redact_data(
|
||||
dataclasses.asdict(await data.system.box.wan_get_info()),
|
||||
TO_REDACT,
|
||||
),
|
||||
"dsl": _async_redact_data(await data.system.box.dsl_get_info()),
|
||||
"ftth": _async_redact_data(await data.system.box.ftth_get_info()),
|
||||
"system": _async_redact_data(await data.system.box.system_get_info()),
|
||||
"wan": _async_redact_data(await data.system.box.wan_get_info()),
|
||||
},
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sfr_box",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["sfrbox-api==0.0.8"]
|
||||
"requirements": ["sfrbox-api==0.0.10"]
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sfrbox_api.models import DslInfo, SystemInfo, WanInfo
|
||||
|
||||
@ -129,7 +130,7 @@ DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = (
|
||||
"unknown",
|
||||
],
|
||||
translation_key="dsl_line_status",
|
||||
value_fn=lambda x: x.line_status.lower().replace(" ", "_"),
|
||||
value_fn=lambda x: _value_to_option(x.line_status),
|
||||
),
|
||||
SFRBoxSensorEntityDescription[DslInfo](
|
||||
key="training",
|
||||
@ -149,7 +150,7 @@ DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = (
|
||||
"unknown",
|
||||
],
|
||||
translation_key="dsl_training",
|
||||
value_fn=lambda x: x.training.lower().replace(" ", "_").replace(".", "_"),
|
||||
value_fn=lambda x: _value_to_option(x.training),
|
||||
),
|
||||
)
|
||||
SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = (
|
||||
@ -181,7 +182,7 @@ SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda x: None if x.temperature is None else x.temperature / 1000,
|
||||
value_fn=lambda x: _get_temperature(x.temperature),
|
||||
),
|
||||
)
|
||||
WAN_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[WanInfo], ...] = (
|
||||
@ -203,23 +204,38 @@ WAN_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[WanInfo], ...] = (
|
||||
)
|
||||
|
||||
|
||||
def _value_to_option(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return value
|
||||
return value.lower().replace(" ", "_").replace(".", "_")
|
||||
|
||||
|
||||
def _get_temperature(value: float | None) -> float | None:
|
||||
if value is None or value < 1000:
|
||||
return value
|
||||
return value / 1000
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the sensors."""
|
||||
data: DomainData = hass.data[DOMAIN][entry.entry_id]
|
||||
system_info = data.system.data
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
|
||||
entities: list[SFRBoxSensor] = [
|
||||
SFRBoxSensor(data.system, description, data.system.data)
|
||||
SFRBoxSensor(data.system, description, system_info)
|
||||
for description in SYSTEM_SENSOR_TYPES
|
||||
]
|
||||
entities.extend(
|
||||
SFRBoxSensor(data.wan, description, data.system.data)
|
||||
SFRBoxSensor(data.wan, description, system_info)
|
||||
for description in WAN_SENSOR_TYPES
|
||||
)
|
||||
if data.system.data.net_infra == "adsl":
|
||||
if system_info.net_infra == "adsl":
|
||||
entities.extend(
|
||||
SFRBoxSensor(data.dsl, description, data.system.data)
|
||||
SFRBoxSensor(data.dsl, description, system_info)
|
||||
for description in DSL_SENSOR_TYPES
|
||||
)
|
||||
|
||||
@ -251,4 +267,6 @@ class SFRBoxSensor[_T](CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SensorEn
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the native value of the device."""
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
@ -9,8 +9,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
@ -40,6 +42,7 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]):
|
||||
|
||||
self.unique_id: str | None = None
|
||||
self.client = Api2(host=host, session=async_get_clientsession(hass))
|
||||
self.legacy_api: int = 0
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Authenticate if needed during initial setup."""
|
||||
@ -60,11 +63,28 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]):
|
||||
info = await self.client.get_info()
|
||||
self.unique_id = format_mac(info.MAC)
|
||||
|
||||
if info.legacy_api:
|
||||
self.legacy_api = info.legacy_api
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"unsupported_firmware",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
learn_more_url="https://smlight.tech/flasher/#SLZB-06",
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="unsupported_firmware",
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> SmData:
|
||||
"""Fetch data from the SMLIGHT device."""
|
||||
try:
|
||||
sensors = Sensors()
|
||||
if not self.legacy_api:
|
||||
sensors = await self.client.get_sensors()
|
||||
|
||||
return SmData(
|
||||
sensors=await self.client.get_sensors(),
|
||||
sensors=sensors,
|
||||
info=await self.client.get_info(),
|
||||
)
|
||||
except SmlightConnectionError as err:
|
||||
|
@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/smlight",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pysmlight==0.0.13"],
|
||||
"requirements": ["pysmlight==0.0.14"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
@ -45,5 +45,11 @@
|
||||
"name": "RAM usage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"unsupported_firmware": {
|
||||
"title": "SLZB core firmware update required",
|
||||
"description": "Your SMLIGHT SLZB-06x device is running an unsupported core firmware version. Please update it to the latest version to enjoy all the features of this integration."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,14 @@ import logging
|
||||
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID, SERVICE_RELOAD
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_NAME,
|
||||
CONF_UNIQUE_ID,
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import ConfigEntryError, HomeAssistantError
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.device import (
|
||||
async_remove_stale_devices_links_keep_current_device,
|
||||
@ -19,7 +24,7 @@ from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_integration
|
||||
|
||||
from .const import CONF_TRIGGER, DOMAIN, PLATFORMS
|
||||
from .const import CONF_MAX, CONF_MIN, CONF_STEP, CONF_TRIGGER, DOMAIN, PLATFORMS
|
||||
from .coordinator import TriggerUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -67,6 +72,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.options.get(CONF_DEVICE_ID),
|
||||
)
|
||||
|
||||
for key in (CONF_MAX, CONF_MIN, CONF_STEP):
|
||||
if key not in entry.options:
|
||||
continue
|
||||
if isinstance(entry.options[key], str):
|
||||
raise ConfigEntryError(
|
||||
f"The '{entry.options.get(CONF_NAME) or ""}' number template needs to "
|
||||
f"be reconfigured, {key} must be a number, got '{entry.options[key]}'"
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, (entry.options["template_type"],)
|
||||
)
|
||||
|
@ -107,15 +107,15 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
|
||||
if domain == Platform.NUMBER:
|
||||
schema |= {
|
||||
vol.Required(CONF_STATE): selector.TemplateSelector(),
|
||||
vol.Required(
|
||||
CONF_MIN, default=f"{{{{{DEFAULT_MIN_VALUE}}}}}"
|
||||
): selector.TemplateSelector(),
|
||||
vol.Required(
|
||||
CONF_MAX, default=f"{{{{{DEFAULT_MAX_VALUE}}}}}"
|
||||
): selector.TemplateSelector(),
|
||||
vol.Required(
|
||||
CONF_STEP, default=f"{{{{{DEFAULT_STEP}}}}}"
|
||||
): selector.TemplateSelector(),
|
||||
vol.Required(CONF_MIN, default=DEFAULT_MIN_VALUE): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
),
|
||||
vol.Required(CONF_MAX, default=DEFAULT_MAX_VALUE): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
),
|
||||
vol.Required(CONF_STEP, default=DEFAULT_STEP): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
),
|
||||
vol.Optional(CONF_SET_VALUE): selector.ActionSelector(),
|
||||
}
|
||||
|
||||
|
@ -28,11 +28,14 @@ PLATFORMS = [
|
||||
Platform.WEATHER,
|
||||
]
|
||||
|
||||
CONF_AVAILABILITY = "availability"
|
||||
CONF_ATTRIBUTES = "attributes"
|
||||
CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
|
||||
CONF_ATTRIBUTES = "attributes"
|
||||
CONF_AVAILABILITY = "availability"
|
||||
CONF_MAX = "max"
|
||||
CONF_MIN = "min"
|
||||
CONF_OBJECT_ID = "object_id"
|
||||
CONF_PICTURE = "picture"
|
||||
CONF_PRESS = "press"
|
||||
CONF_OBJECT_ID = "object_id"
|
||||
CONF_STEP = "step"
|
||||
CONF_TURN_OFF = "turn_off"
|
||||
CONF_TURN_ON = "turn_on"
|
||||
|
@ -31,7 +31,7 @@ from homeassistant.helpers.script import Script
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
@ -42,9 +42,6 @@ from .trigger_entity import TriggerEntity
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SET_VALUE = "set_value"
|
||||
CONF_MIN = "min"
|
||||
CONF_MAX = "max"
|
||||
CONF_STEP = "step"
|
||||
|
||||
DEFAULT_NAME = "Template Number"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
@ -36,8 +36,10 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up connection to Yale."""
|
||||
try:
|
||||
self.yale = YaleSmartAlarmClient(
|
||||
self.entry.data[CONF_USERNAME], self.entry.data[CONF_PASSWORD]
|
||||
self.yale = await self.hass.async_add_executor_job(
|
||||
YaleSmartAlarmClient,
|
||||
self.entry.data[CONF_USERNAME],
|
||||
self.entry.data[CONF_PASSWORD],
|
||||
)
|
||||
except AuthenticationError as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@ -130,33 +129,6 @@ def _discovery(config_info):
|
||||
zones.extend(recv.zone_controllers())
|
||||
else:
|
||||
_LOGGER.debug("Config Zones")
|
||||
zones = None
|
||||
|
||||
# Fix for upstream issues in rxv.find() with some hardware.
|
||||
with contextlib.suppress(AttributeError, ValueError):
|
||||
for recv in rxv.find(DISCOVER_TIMEOUT):
|
||||
_LOGGER.debug(
|
||||
"Found Serial %s %s %s",
|
||||
recv.serial_number,
|
||||
recv.ctrl_url,
|
||||
recv.zone,
|
||||
)
|
||||
if recv.ctrl_url == config_info.ctrl_url:
|
||||
_LOGGER.debug(
|
||||
"Config Zones Matched Serial %s: %s",
|
||||
recv.ctrl_url,
|
||||
recv.serial_number,
|
||||
)
|
||||
zones = rxv.RXV(
|
||||
config_info.ctrl_url,
|
||||
friendly_name=config_info.name,
|
||||
serial_number=recv.serial_number,
|
||||
model_name=recv.model_name,
|
||||
).zone_controllers()
|
||||
break
|
||||
|
||||
if not zones:
|
||||
_LOGGER.debug("Config Zones Fallback")
|
||||
zones = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers()
|
||||
|
||||
_LOGGER.debug("Returned _discover zones: %s", zones)
|
||||
|
@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 9
|
||||
PATCH_VERSION: Final = "0"
|
||||
PATCH_VERSION: Final = "1"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||
|
@ -31,7 +31,7 @@ habluetooth==3.4.0
|
||||
hass-nabucasa==0.81.1
|
||||
hassil==1.7.4
|
||||
home-assistant-bluetooth==1.12.2
|
||||
home-assistant-frontend==20240904.0
|
||||
home-assistant-frontend==20240906.0
|
||||
home-assistant-intents==2024.9.4
|
||||
httpx==0.27.0
|
||||
ifaddr==0.2.0
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.9.0"
|
||||
version = "2024.9.1"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -350,7 +350,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.41
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==2.3.2
|
||||
aiorussound==3.0.4
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@ -1102,7 +1102,7 @@ hole==0.8.0
|
||||
holidays==0.56
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240904.0
|
||||
home-assistant-frontend==20240906.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.9.4
|
||||
@ -1744,7 +1744,7 @@ pyatag==0.3.5.3
|
||||
pyatmo==8.1.0
|
||||
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.15.0
|
||||
pyatv==0.15.1
|
||||
|
||||
# homeassistant.components.aussie_broadband
|
||||
pyaussiebb==0.0.15
|
||||
@ -2109,7 +2109,7 @@ pyownet==0.10.0.post1
|
||||
pypca==0.0.7
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.7.21
|
||||
pypck==0.7.22
|
||||
|
||||
# homeassistant.components.pjlink
|
||||
pypjlink2==1.2.1
|
||||
@ -2214,7 +2214,7 @@ pysmartthings==0.7.8
|
||||
pysml==0.0.12
|
||||
|
||||
# homeassistant.components.smlight
|
||||
pysmlight==0.0.13
|
||||
pysmlight==0.0.14
|
||||
|
||||
# homeassistant.components.snmp
|
||||
pysnmp==6.2.5
|
||||
@ -2595,7 +2595,7 @@ sensorpush-ble==1.6.2
|
||||
sentry-sdk==1.40.3
|
||||
|
||||
# homeassistant.components.sfr_box
|
||||
sfrbox-api==0.0.8
|
||||
sfrbox-api==0.0.10
|
||||
|
||||
# homeassistant.components.sharkiq
|
||||
sharkiq==1.0.2
|
||||
|
@ -332,7 +332,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.41
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==2.3.2
|
||||
aiorussound==3.0.4
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@ -925,7 +925,7 @@ hole==0.8.0
|
||||
holidays==0.56
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240904.0
|
||||
home-assistant-frontend==20240906.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.9.4
|
||||
@ -1412,7 +1412,7 @@ pyatag==0.3.5.3
|
||||
pyatmo==8.1.0
|
||||
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.15.0
|
||||
pyatv==0.15.1
|
||||
|
||||
# homeassistant.components.aussie_broadband
|
||||
pyaussiebb==0.0.15
|
||||
@ -1687,7 +1687,7 @@ pyoverkiz==1.13.14
|
||||
pyownet==0.10.0.post1
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.7.21
|
||||
pypck==0.7.22
|
||||
|
||||
# homeassistant.components.pjlink
|
||||
pypjlink2==1.2.1
|
||||
@ -1768,7 +1768,7 @@ pysmartthings==0.7.8
|
||||
pysml==0.0.12
|
||||
|
||||
# homeassistant.components.smlight
|
||||
pysmlight==0.0.13
|
||||
pysmlight==0.0.14
|
||||
|
||||
# homeassistant.components.snmp
|
||||
pysnmp==6.2.5
|
||||
@ -2053,7 +2053,7 @@ sensorpush-ble==1.6.2
|
||||
sentry-sdk==1.40.3
|
||||
|
||||
# homeassistant.components.sfr_box
|
||||
sfrbox-api==0.0.8
|
||||
sfrbox-api==0.0.10
|
||||
|
||||
# homeassistant.components.sharkiq
|
||||
sharkiq==1.0.2
|
||||
|
@ -79,6 +79,7 @@ async def test_full_flow(
|
||||
("exception", "error"),
|
||||
[
|
||||
(ApiException, "cannot_connect"),
|
||||
(TimeoutError, "cannot_connect"),
|
||||
(AuthenticationFailed, "invalid_auth"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
|
@ -1,10 +1,19 @@
|
||||
"""Test BTHome BLE events."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.components.bluetooth import DOMAIN as BLUETOOTH_DOMAIN
|
||||
from homeassistant.components.bthome.const import CONF_SUBTYPE, DOMAIN
|
||||
from homeassistant.components.device_automation import DeviceAutomationType
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_DOMAIN,
|
||||
CONF_PLATFORM,
|
||||
CONF_TYPE,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.setup import async_setup_component
|
||||
@ -121,6 +130,117 @@ async def test_get_triggers_button(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_get_triggers_multiple_buttons(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
) -> None:
|
||||
"""Test that we get the expected triggers for multiple buttons device."""
|
||||
mac = "A4:C1:38:8D:18:B2"
|
||||
entry = await _async_setup_bthome_device(hass, mac)
|
||||
events = async_capture_events(hass, "bthome_ble_event")
|
||||
|
||||
# Emit button_1 long press and button_2 press events
|
||||
# so it creates the device in the registry
|
||||
inject_bluetooth_service_info_bleak(
|
||||
hass,
|
||||
make_bthome_v2_adv(mac, b"\x40\x3a\x04\x3a\x01"),
|
||||
)
|
||||
|
||||
# wait for the event
|
||||
await hass.async_block_till_done()
|
||||
assert len(events) == 2
|
||||
|
||||
device = device_registry.async_get_device(identifiers={get_device_id(mac)})
|
||||
assert device
|
||||
expected_trigger1 = {
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_DEVICE_ID: device.id,
|
||||
CONF_TYPE: "button_1",
|
||||
CONF_SUBTYPE: "long_press",
|
||||
"metadata": {},
|
||||
}
|
||||
expected_trigger2 = {
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_DEVICE_ID: device.id,
|
||||
CONF_TYPE: "button_2",
|
||||
CONF_SUBTYPE: "press",
|
||||
"metadata": {},
|
||||
}
|
||||
triggers = await async_get_device_automations(
|
||||
hass, DeviceAutomationType.TRIGGER, device.id
|
||||
)
|
||||
assert expected_trigger1 in triggers
|
||||
assert expected_trigger2 in triggers
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("event_class", "event_type", "expected"),
|
||||
[
|
||||
("button_1", "long_press", STATE_ON),
|
||||
("button_2", "press", STATE_ON),
|
||||
("button_3", "long_press", STATE_UNAVAILABLE),
|
||||
("button", "long_press", STATE_UNAVAILABLE),
|
||||
("button_1", "invalid_press", STATE_UNAVAILABLE),
|
||||
],
|
||||
)
|
||||
async def test_validate_trigger_config(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
event_class: str,
|
||||
event_type: str,
|
||||
expected: str,
|
||||
) -> None:
|
||||
"""Test unsupported trigger does not return a trigger config."""
|
||||
mac = "A4:C1:38:8D:18:B2"
|
||||
entry = await _async_setup_bthome_device(hass, mac)
|
||||
|
||||
# Emit button_1 long press and button_2 press events
|
||||
# so it creates the device in the registry
|
||||
inject_bluetooth_service_info_bleak(
|
||||
hass,
|
||||
make_bthome_v2_adv(mac, b"\x40\x3a\x04\x3a\x01"),
|
||||
)
|
||||
|
||||
# wait for the event
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={get_device_id(mac)})
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: [
|
||||
{
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_DEVICE_ID: device.id,
|
||||
CONF_TYPE: event_class,
|
||||
CONF_SUBTYPE: event_type,
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {"some": "test_trigger_button_long_press"},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
automations = hass.states.async_entity_ids(automation.DOMAIN)
|
||||
assert len(automations) == 1
|
||||
assert hass.states.get(automations[0]).state == expected
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_get_triggers_dimmer(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
) -> None:
|
||||
@ -235,7 +355,7 @@ async def test_if_fires_on_motion_detected(
|
||||
make_bthome_v2_adv(mac, b"\x40\x3a\x03"),
|
||||
)
|
||||
|
||||
# # wait for the event
|
||||
# wait for the event
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={get_device_id(mac)})
|
||||
|
@ -175,7 +175,7 @@ async def test_light_turn_on_service(
|
||||
assert len(mock_bridge_v2.mock_requests) == 6
|
||||
assert mock_bridge_v2.mock_requests[5]["json"]["color_temperature"]["mirek"] == 500
|
||||
|
||||
# test enable effect
|
||||
# test enable an effect
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
@ -184,8 +184,20 @@ async def test_light_turn_on_service(
|
||||
)
|
||||
assert len(mock_bridge_v2.mock_requests) == 7
|
||||
assert mock_bridge_v2.mock_requests[6]["json"]["effects"]["effect"] == "candle"
|
||||
# fire event to update effect in HA state
|
||||
event = {
|
||||
"id": "3a6710fa-4474-4eba-b533-5e6e72968feb",
|
||||
"type": "light",
|
||||
"effects": {"status": "candle"},
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", event)
|
||||
await hass.async_block_till_done()
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.attributes["effect"] == "candle"
|
||||
|
||||
# test disable effect
|
||||
# it should send a request with effect set to "no_effect"
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
@ -194,6 +206,28 @@ async def test_light_turn_on_service(
|
||||
)
|
||||
assert len(mock_bridge_v2.mock_requests) == 8
|
||||
assert mock_bridge_v2.mock_requests[7]["json"]["effects"]["effect"] == "no_effect"
|
||||
# fire event to update effect in HA state
|
||||
event = {
|
||||
"id": "3a6710fa-4474-4eba-b533-5e6e72968feb",
|
||||
"type": "light",
|
||||
"effects": {"status": "no_effect"},
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", event)
|
||||
await hass.async_block_till_done()
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.attributes["effect"] == "None"
|
||||
|
||||
# test turn on with useless effect
|
||||
# it should send a effect in the request if the device has no effect active
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": test_light_id, "effect": "None"},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(mock_bridge_v2.mock_requests) == 9
|
||||
assert "effects" not in mock_bridge_v2.mock_requests[8]["json"]
|
||||
|
||||
# test timed effect
|
||||
await hass.services.async_call(
|
||||
@ -202,11 +236,11 @@ async def test_light_turn_on_service(
|
||||
{"entity_id": test_light_id, "effect": "sunrise", "transition": 6},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(mock_bridge_v2.mock_requests) == 9
|
||||
assert len(mock_bridge_v2.mock_requests) == 10
|
||||
assert (
|
||||
mock_bridge_v2.mock_requests[8]["json"]["timed_effects"]["effect"] == "sunrise"
|
||||
mock_bridge_v2.mock_requests[9]["json"]["timed_effects"]["effect"] == "sunrise"
|
||||
)
|
||||
assert mock_bridge_v2.mock_requests[8]["json"]["timed_effects"]["duration"] == 6000
|
||||
assert mock_bridge_v2.mock_requests[9]["json"]["timed_effects"]["duration"] == 6000
|
||||
|
||||
# test enabling effect should ignore color temperature
|
||||
await hass.services.async_call(
|
||||
@ -215,9 +249,9 @@ async def test_light_turn_on_service(
|
||||
{"entity_id": test_light_id, "effect": "candle", "color_temp": 500},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(mock_bridge_v2.mock_requests) == 10
|
||||
assert mock_bridge_v2.mock_requests[9]["json"]["effects"]["effect"] == "candle"
|
||||
assert "color_temperature" not in mock_bridge_v2.mock_requests[9]["json"]
|
||||
assert len(mock_bridge_v2.mock_requests) == 11
|
||||
assert mock_bridge_v2.mock_requests[10]["json"]["effects"]["effect"] == "candle"
|
||||
assert "color_temperature" not in mock_bridge_v2.mock_requests[10]["json"]
|
||||
|
||||
# test enabling effect should ignore xy color
|
||||
await hass.services.async_call(
|
||||
@ -226,9 +260,9 @@ async def test_light_turn_on_service(
|
||||
{"entity_id": test_light_id, "effect": "candle", "xy_color": [0.123, 0.123]},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(mock_bridge_v2.mock_requests) == 11
|
||||
assert mock_bridge_v2.mock_requests[10]["json"]["effects"]["effect"] == "candle"
|
||||
assert "xy_color" not in mock_bridge_v2.mock_requests[9]["json"]
|
||||
assert len(mock_bridge_v2.mock_requests) == 12
|
||||
assert mock_bridge_v2.mock_requests[11]["json"]["effects"]["effect"] == "candle"
|
||||
assert "xy_color" not in mock_bridge_v2.mock_requests[11]["json"]
|
||||
|
||||
|
||||
async def test_light_turn_off_service(
|
||||
|
@ -37,10 +37,10 @@ def mock_russound() -> Generator[AsyncMock]:
|
||||
"""Mock the Russound RIO client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.russound_rio.Russound", autospec=True
|
||||
"homeassistant.components.russound_rio.RussoundClient", autospec=True
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.russound_rio.config_flow.Russound",
|
||||
"homeassistant.components.russound_rio.config_flow.RussoundClient",
|
||||
return_value=mock_client,
|
||||
),
|
||||
):
|
||||
|
@ -31,7 +31,7 @@
|
||||
'product_id': 'NB6VAC-FXC-r0',
|
||||
'refclient': '',
|
||||
'serial_number': '**REDACTED**',
|
||||
'temperature': 27560,
|
||||
'temperature': 27560.0,
|
||||
'uptime': 2353575,
|
||||
'version_bootloader': 'NB6VAC-BOOTLOADER-R4.0.8',
|
||||
'version_dsldriver': 'NB6VAC-XDSL-A2pv6F039p',
|
||||
@ -90,7 +90,7 @@
|
||||
'product_id': 'NB6VAC-FXC-r0',
|
||||
'refclient': '',
|
||||
'serial_number': '**REDACTED**',
|
||||
'temperature': 27560,
|
||||
'temperature': 27560.0,
|
||||
'uptime': 2353575,
|
||||
'version_bootloader': 'NB6VAC-BOOTLOADER-R4.0.8',
|
||||
'version_dsldriver': 'NB6VAC-XDSL-A2pv6F039p',
|
||||
|
@ -3,10 +3,12 @@
|
||||
"device_ip": "192.168.1.161",
|
||||
"fs_total": 3456,
|
||||
"fw_channel": "dev",
|
||||
"legacy_api": 0,
|
||||
"hostname": "SLZB-06p7",
|
||||
"MAC": "AA:BB:CC:DD:EE:FF",
|
||||
"model": "SLZB-06p7",
|
||||
"ram_total": 296,
|
||||
"sw_version": "v2.3.1.dev",
|
||||
"sw_version": "v2.3.6",
|
||||
"wifi_mode": 0,
|
||||
"zb_flash_size": 704,
|
||||
"zb_hw": "CC2652P7",
|
||||
|
@ -27,7 +27,7 @@
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': 'core: v2.3.1.dev / zigbee: -1',
|
||||
'sw_version': 'core: v2.3.6 / zigbee: -1',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
|
@ -3,15 +3,17 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError
|
||||
from pysmlight import Info
|
||||
from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError, SmlightError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.smlight.const import SCAN_INTERVAL
|
||||
from homeassistant.components.smlight.const import DOMAIN, SCAN_INTERVAL
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.issue_registry import IssueRegistry
|
||||
|
||||
from .conftest import setup_integration
|
||||
|
||||
@ -92,3 +94,33 @@ async def test_device_info(
|
||||
)
|
||||
assert device_entry is not None
|
||||
assert device_entry == snapshot
|
||||
|
||||
|
||||
async def test_device_legacy_firmware(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_smlight_client: MagicMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
issue_registry: IssueRegistry,
|
||||
) -> None:
|
||||
"""Test device setup for old firmware version that dont support required API."""
|
||||
LEGACY_VERSION = "v2.3.1"
|
||||
mock_smlight_client.get_sensors.side_effect = SmlightError
|
||||
mock_smlight_client.get_info.return_value = Info(
|
||||
legacy_api=1, sw_version=LEGACY_VERSION, MAC="AA:BB:CC:DD:EE:FF"
|
||||
)
|
||||
entry = await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff"
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)}
|
||||
)
|
||||
assert LEGACY_VERSION in device_entry.sw_version
|
||||
|
||||
issue = issue_registry.async_get_issue(
|
||||
domain=DOMAIN, issue_id="unsupported_firmware"
|
||||
)
|
||||
assert issue is not None
|
||||
assert issue.domain == DOMAIN
|
||||
assert issue.issue_id == "unsupported_firmware"
|
||||
|
@ -98,9 +98,9 @@ from tests.typing import WebSocketGenerator
|
||||
{"one": "30.0", "two": "20.0"},
|
||||
{},
|
||||
{
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": "0",
|
||||
"max": "100",
|
||||
"step": "0.1",
|
||||
"set_value": {
|
||||
"action": "input_number.set_value",
|
||||
"target": {"entity_id": "input_number.test"},
|
||||
@ -108,9 +108,9 @@ from tests.typing import WebSocketGenerator
|
||||
},
|
||||
},
|
||||
{
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
"set_value": {
|
||||
"action": "input_number.set_value",
|
||||
"target": {"entity_id": "input_number.test"},
|
||||
@ -258,14 +258,14 @@ async def test_config_flow(
|
||||
"number",
|
||||
{"state": "{{ states('number.one') }}"},
|
||||
{
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": "0",
|
||||
"max": "100",
|
||||
"step": "0.1",
|
||||
},
|
||||
{
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
},
|
||||
),
|
||||
(
|
||||
@ -451,9 +451,9 @@ def get_suggested(schema, key):
|
||||
["30.0", "20.0"],
|
||||
{"one": "30.0", "two": "20.0"},
|
||||
{
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
"set_value": {
|
||||
"action": "input_number.set_value",
|
||||
"target": {"entity_id": "input_number.test"},
|
||||
@ -461,9 +461,9 @@ def get_suggested(schema, key):
|
||||
},
|
||||
},
|
||||
{
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
"set_value": {
|
||||
"action": "input_number.set_value",
|
||||
"target": {"entity_id": "input_number.test"},
|
||||
@ -1230,14 +1230,14 @@ async def test_option_flow_sensor_preview_config_entry_removed(
|
||||
"number",
|
||||
{"state": "{{ states('number.one') }}"},
|
||||
{
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
},
|
||||
{
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
},
|
||||
),
|
||||
(
|
||||
|
@ -319,9 +319,9 @@ async def async_yaml_patch_helper(hass: HomeAssistant, filename: str) -> None:
|
||||
"template_type": "number",
|
||||
"name": "My template",
|
||||
"state": "{{ 10 }}",
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
"set_value": {
|
||||
"action": "input_number.set_value",
|
||||
"target": {"entity_id": "input_number.test"},
|
||||
@ -330,9 +330,9 @@ async def async_yaml_patch_helper(hass: HomeAssistant, filename: str) -> None:
|
||||
},
|
||||
{
|
||||
"state": "{{ 11 }}",
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
"set_value": {
|
||||
"action": "input_number.set_value",
|
||||
"target": {"entity_id": "input_number.test"},
|
||||
@ -454,3 +454,40 @@ async def test_change_device(
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
async def test_fail_non_numerical_number_settings(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test that non numerical number options causes config entry setup to fail.
|
||||
|
||||
Support for non numerical max, min and step was added in HA Core 2024.9.0 and
|
||||
removed in HA Core 2024.9.1.
|
||||
"""
|
||||
|
||||
options = {
|
||||
"template_type": "number",
|
||||
"name": "My template",
|
||||
"state": "{{ 10 }}",
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"set_value": {
|
||||
"action": "input_number.set_value",
|
||||
"target": {"entity_id": "input_number.test"},
|
||||
"data": {"value": "{{ value }}"},
|
||||
},
|
||||
}
|
||||
# Setup the config entry
|
||||
template_config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options=options,
|
||||
title="Template",
|
||||
)
|
||||
template_config_entry.add_to_hass(hass)
|
||||
assert not await hass.config_entries.async_setup(template_config_entry.entry_id)
|
||||
assert (
|
||||
"The 'My template' number template needs to be reconfigured, "
|
||||
"max must be a number, got '{{ 100 }}'" in caplog.text
|
||||
)
|
||||
|
@ -58,9 +58,9 @@ async def test_setup_config_entry(
|
||||
"name": "My template",
|
||||
"template_type": "number",
|
||||
"state": "{{ 10 }}",
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
"set_value": {
|
||||
"action": "input_number.set_value",
|
||||
"target": {"entity_id": "input_number.test"},
|
||||
@ -524,9 +524,9 @@ async def test_device_id(
|
||||
"name": "My template",
|
||||
"template_type": "number",
|
||||
"state": "{{ 10 }}",
|
||||
"min": "{{ 0 }}",
|
||||
"max": "{{ 100 }}",
|
||||
"step": "{{ 0.1 }}",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.1,
|
||||
"set_value": {
|
||||
"action": "input_number.set_value",
|
||||
"target": {"entity_id": "input_number.test"},
|
||||
|
Loading…
x
Reference in New Issue
Block a user