This commit is contained in:
Franck Nijhof 2025-04-19 12:22:36 +02:00 committed by GitHub
commit 6f0a9910ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 3171 additions and 140 deletions

View File

@ -120,6 +120,7 @@ class AppleTvMediaPlayer(
"""Initialize the Apple TV media player.""" """Initialize the Apple TV media player."""
super().__init__(name, identifier, manager) super().__init__(name, identifier, manager)
self._playing: Playing | None = None self._playing: Playing | None = None
self._playing_last_updated: datetime | None = None
self._app_list: dict[str, str] = {} self._app_list: dict[str, str] = {}
@callback @callback
@ -209,6 +210,7 @@ class AppleTvMediaPlayer(
This is a callback function from pyatv.interface.PushListener. This is a callback function from pyatv.interface.PushListener.
""" """
self._playing = playstatus self._playing = playstatus
self._playing_last_updated = dt_util.utcnow()
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
@ -316,7 +318,7 @@ class AppleTvMediaPlayer(
def media_position_updated_at(self) -> datetime | None: def media_position_updated_at(self) -> datetime | None:
"""Last valid time of media position.""" """Last valid time of media position."""
if self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}: if self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}:
return dt_util.utcnow() return self._playing_last_updated
return None return None
async def async_play_media( async def async_play_media(

View File

@ -81,4 +81,7 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if switch is on.""" """Return True if switch is on."""
return self.coordinator.data[OTHER][self._device.index].status == STATE_ON return (
self.coordinator.data[self._device.type][self._device.index].status
== STATE_ON
)

View File

@ -8,7 +8,7 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["devolo_plc_api"], "loggers": ["devolo_plc_api"],
"requirements": ["devolo-plc-api==1.4.1"], "requirements": ["devolo-plc-api==1.5.1"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_dvl-deviceapi._tcp.local.", "type": "_dvl-deviceapi._tcp.local.",

View File

@ -179,22 +179,18 @@ class DukeEnergyCoordinator(DataUpdateCoordinator[None]):
one = timedelta(days=1) one = timedelta(days=1)
if start_time is None: if start_time is None:
# Max 3 years of data # Max 3 years of data
agreement_date = dt_util.parse_datetime(meter["agreementActiveDate"]) start = dt_util.now(tz) - timedelta(days=3 * 365)
if agreement_date is None:
start = dt_util.now(tz) - timedelta(days=3 * 365)
else:
start = max(
agreement_date.replace(tzinfo=tz),
dt_util.now(tz) - timedelta(days=3 * 365),
)
else: else:
start = datetime.fromtimestamp(start_time, tz=tz) - lookback start = datetime.fromtimestamp(start_time, tz=tz) - lookback
agreement_date = dt_util.parse_datetime(meter["agreementActiveDate"])
if agreement_date is not None:
start = max(agreement_date.replace(tzinfo=tz), start)
start = start.replace(hour=0, minute=0, second=0, microsecond=0) start = start.replace(hour=0, minute=0, second=0, microsecond=0)
end = dt_util.now(tz).replace(hour=0, minute=0, second=0, microsecond=0) - one end = dt_util.now(tz).replace(hour=0, minute=0, second=0, microsecond=0) - one
_LOGGER.debug("Data lookup range: %s - %s", start, end) _LOGGER.debug("Data lookup range: %s - %s", start, end)
start_step = end - lookback start_step = max(end - lookback, start)
end_step = end end_step = end
usage: dict[datetime, dict[str, float | int]] = {} usage: dict[datetime, dict[str, float | int]] = {}
while True: while True:

View File

@ -35,7 +35,7 @@ async def validate_input(data):
lon = weather_data.lon lon = weather_data.lon
return { return {
CONF_TITLE: weather_data.metadata.get("location"), CONF_TITLE: weather_data.metadata.location,
CONF_STATION: weather_data.station_id, CONF_STATION: weather_data.station_id,
CONF_LATITUDE: lat, CONF_LATITUDE: lat,
CONF_LONGITUDE: lon, CONF_LONGITUDE: lon,

View File

@ -7,7 +7,7 @@ from datetime import timedelta
import logging import logging
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from env_canada import ECAirQuality, ECRadar, ECWeather, ec_exc from env_canada import ECAirQuality, ECRadar, ECWeather, ECWeatherUpdateFailed, ec_exc
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -65,6 +65,6 @@ class ECDataUpdateCoordinator[DataT: ECDataType](DataUpdateCoordinator[DataT]):
"""Fetch data from EC.""" """Fetch data from EC."""
try: try:
await self.ec_data.update() await self.ec_data.update()
except (ET.ParseError, ec_exc.UnknownStationId) as ex: except (ET.ParseError, ECWeatherUpdateFailed, ec_exc.UnknownStationId) as ex:
raise UpdateFailed(f"Error fetching {self.name} data: {ex}") from ex raise UpdateFailed(f"Error fetching {self.name} data: {ex}") from ex
return self.ec_data return self.ec_data

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["env_canada"], "loggers": ["env_canada"],
"requirements": ["env-canada==0.8.0"] "requirements": ["env-canada==0.10.1"]
} }

View File

@ -145,7 +145,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = (
key="timestamp", key="timestamp",
translation_key="timestamp", translation_key="timestamp",
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.metadata.get("timestamp"), value_fn=lambda data: data.metadata.timestamp,
), ),
ECSensorEntityDescription( ECSensorEntityDescription(
key="uv_index", key="uv_index",
@ -289,7 +289,7 @@ class ECBaseSensorEntity[DataT: ECDataType](
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description = description self.entity_description = description
self._ec_data = coordinator.ec_data self._ec_data = coordinator.ec_data
self._attr_attribution = self._ec_data.metadata["attribution"] self._attr_attribution = self._ec_data.metadata.attribution
self._attr_unique_id = f"{coordinator.config_entry.title}-{description.key}" self._attr_unique_id = f"{coordinator.config_entry.title}-{description.key}"
self._attr_device_info = coordinator.device_info self._attr_device_info = coordinator.device_info
@ -313,8 +313,8 @@ class ECSensorEntity[DataT: ECDataType](ECBaseSensorEntity[DataT]):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator, description) super().__init__(coordinator, description)
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {
ATTR_LOCATION: self._ec_data.metadata.get("location"), ATTR_LOCATION: self._ec_data.metadata.location,
ATTR_STATION: self._ec_data.metadata.get("station"), ATTR_STATION: self._ec_data.metadata.station,
} }
@ -329,8 +329,8 @@ class ECAlertSensorEntity(ECBaseSensorEntity[ECWeather]):
return None return None
extra_state_attrs = { extra_state_attrs = {
ATTR_LOCATION: self._ec_data.metadata.get("location"), ATTR_LOCATION: self._ec_data.metadata.location,
ATTR_STATION: self._ec_data.metadata.get("station"), ATTR_STATION: self._ec_data.metadata.station,
} }
for index, alert in enumerate(value, start=1): for index, alert in enumerate(value, start=1):
extra_state_attrs[f"alert_{index}"] = alert.get("title") extra_state_attrs[f"alert_{index}"] = alert.get("title")

View File

@ -115,7 +115,7 @@ class ECWeatherEntity(
"""Initialize Environment Canada weather.""" """Initialize Environment Canada weather."""
super().__init__(coordinator) super().__init__(coordinator)
self.ec_data = coordinator.ec_data self.ec_data = coordinator.ec_data
self._attr_attribution = self.ec_data.metadata["attribution"] self._attr_attribution = self.ec_data.metadata.attribution
self._attr_translation_key = "forecast" self._attr_translation_key = "forecast"
self._attr_unique_id = _calculate_unique_id( self._attr_unique_id = _calculate_unique_id(
coordinator.config_entry.unique_id, False coordinator.config_entry.unique_id, False

View File

@ -74,7 +74,7 @@ def build_rrule(task: TaskData) -> rrule:
bysetpos = None bysetpos = None
if rrule_frequency == MONTHLY and task.weeksOfMonth: if rrule_frequency == MONTHLY and task.weeksOfMonth:
bysetpos = task.weeksOfMonth bysetpos = [i + 1 for i in task.weeksOfMonth]
weekdays = weekdays if weekdays else [MO] weekdays = weekdays if weekdays else [MO]
return rrule( return rrule(

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday", "documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["holidays==0.69", "babel==2.15.0"] "requirements": ["holidays==0.70", "babel==2.15.0"]
} }

View File

@ -204,7 +204,7 @@ class HomeConnectCoordinator(
events = self.data[event_message_ha_id].events events = self.data[event_message_ha_id].events
for event in event_message.data.items: for event in event_message.data.items:
event_key = event.key event_key = event.key
if event_key in SettingKey: if event_key in SettingKey.__members__.values(): # type: ignore[comparison-overlap]
setting_key = SettingKey(event_key) setting_key = SettingKey(event_key)
if setting_key in settings: if setting_key in settings:
settings[setting_key].value = event.value settings[setting_key].value = event.value

View File

@ -61,11 +61,14 @@ OPTIONS_SCHEMA = vol.Schema(
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _get_data_schema(hass: HomeAssistant) -> vol.Schema: async def _get_data_schema(hass: HomeAssistant) -> vol.Schema:
default_location = { default_location = {
CONF_LATITUDE: hass.config.latitude, CONF_LATITUDE: hass.config.latitude,
CONF_LONGITUDE: hass.config.longitude, CONF_LONGITUDE: hass.config.longitude,
} }
get_timezones: list[str] = list(
await hass.async_add_executor_job(zoneinfo.available_timezones)
)
return vol.Schema( return vol.Schema(
{ {
vol.Required(CONF_DIASPORA, default=DEFAULT_DIASPORA): BooleanSelector(), vol.Required(CONF_DIASPORA, default=DEFAULT_DIASPORA): BooleanSelector(),
@ -75,9 +78,7 @@ def _get_data_schema(hass: HomeAssistant) -> vol.Schema:
vol.Optional(CONF_LOCATION, default=default_location): LocationSelector(), vol.Optional(CONF_LOCATION, default=default_location): LocationSelector(),
vol.Optional(CONF_ELEVATION, default=hass.config.elevation): int, vol.Optional(CONF_ELEVATION, default=hass.config.elevation): int,
vol.Optional(CONF_TIME_ZONE, default=hass.config.time_zone): SelectSelector( vol.Optional(CONF_TIME_ZONE, default=hass.config.time_zone): SelectSelector(
SelectSelectorConfig( SelectSelectorConfig(options=get_timezones, sort=True)
options=sorted(zoneinfo.available_timezones()),
)
), ),
} }
) )
@ -109,7 +110,7 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=self.add_suggested_values_to_schema( data_schema=self.add_suggested_values_to_schema(
_get_data_schema(self.hass), user_input await _get_data_schema(self.hass), user_input
), ),
) )
@ -121,7 +122,7 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
if not user_input: if not user_input:
return self.async_show_form( return self.async_show_form(
data_schema=self.add_suggested_values_to_schema( data_schema=self.add_suggested_values_to_schema(
_get_data_schema(self.hass), await _get_data_schema(self.hass),
reconfigure_entry.data, reconfigure_entry.data,
), ),
step_id="reconfigure", step_id="reconfigure",

View File

@ -145,7 +145,10 @@ class KrakenData:
await asyncio.sleep(CALL_RATE_LIMIT_SLEEP) await asyncio.sleep(CALL_RATE_LIMIT_SLEEP)
def _get_websocket_name_asset_pairs(self) -> str: def _get_websocket_name_asset_pairs(self) -> str:
return ",".join(wsname for wsname in self.tradable_asset_pairs.values()) return ",".join(
self.tradable_asset_pairs[tracked_pair]
for tracked_pair in self._config_entry.options[CONF_TRACKED_ASSET_PAIRS]
)
def set_update_interval(self, update_interval: int) -> None: def set_update_interval(self, update_interval: int) -> None:
"""Set the coordinator update_interval to the supplied update_interval.""" """Set the coordinator update_interval to the supplied update_interval."""

View File

@ -254,7 +254,7 @@ def _generate_device_config(
comp_config = config[CONF_COMPONENTS] comp_config = config[CONF_COMPONENTS]
for platform, discover_id in mqtt_data.discovery_already_discovered: for platform, discover_id in mqtt_data.discovery_already_discovered:
ids = discover_id.split(" ") ids = discover_id.split(" ")
component_node_id = ids.pop(0) component_node_id = f"{ids.pop(1)} {ids.pop(0)}" if len(ids) > 2 else ids.pop(0)
component_object_id = " ".join(ids) component_object_id = " ".join(ids)
if not ids: if not ids:
continue continue

View File

@ -371,6 +371,9 @@ def migrate_entity_ids(
new_device_id = f"{host.unique_id}" new_device_id = f"{host.unique_id}"
else: else:
new_device_id = f"{host.unique_id}_{device_uid[1]}" new_device_id = f"{host.unique_id}_{device_uid[1]}"
_LOGGER.debug(
"Updating Reolink device UID from %s to %s", device_uid, new_device_id
)
new_identifiers = {(DOMAIN, new_device_id)} new_identifiers = {(DOMAIN, new_device_id)}
device_reg.async_update_device(device.id, new_identifiers=new_identifiers) device_reg.async_update_device(device.id, new_identifiers=new_identifiers)
@ -383,6 +386,9 @@ def migrate_entity_ids(
new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}" new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}"
else: else:
new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}"
_LOGGER.debug(
"Updating Reolink device UID from %s to %s", device_uid, new_device_id
)
new_identifiers = {(DOMAIN, new_device_id)} new_identifiers = {(DOMAIN, new_device_id)}
existing_device = device_reg.async_get_device(identifiers=new_identifiers) existing_device = device_reg.async_get_device(identifiers=new_identifiers)
if existing_device is None: if existing_device is None:
@ -415,6 +421,11 @@ def migrate_entity_ids(
host.unique_id host.unique_id
): ):
new_id = f"{host.unique_id}_{entity.unique_id.split('_', 1)[1]}" new_id = f"{host.unique_id}_{entity.unique_id.split('_', 1)[1]}"
_LOGGER.debug(
"Updating Reolink entity unique_id from %s to %s",
entity.unique_id,
new_id,
)
entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id) entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id)
if entity.device_id in ch_device_ids: if entity.device_id in ch_device_ids:
@ -430,6 +441,11 @@ def migrate_entity_ids(
continue continue
if host.api.supported(ch, "UID") and id_parts[1] != host.api.camera_uid(ch): if host.api.supported(ch, "UID") and id_parts[1] != host.api.camera_uid(ch):
new_id = f"{host.unique_id}_{host.api.camera_uid(ch)}_{id_parts[2]}" new_id = f"{host.unique_id}_{host.api.camera_uid(ch)}_{id_parts[2]}"
_LOGGER.debug(
"Updating Reolink entity unique_id from %s to %s",
entity.unique_id,
new_id,
)
existing_entity = entity_reg.async_get_entity_id( existing_entity = entity_reg.async_get_entity_id(
entity.domain, entity.platform, new_id entity.domain, entity.platform, new_id
) )

View File

@ -19,5 +19,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["reolink_aio"], "loggers": ["reolink_aio"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["reolink-aio==0.13.1"] "requirements": ["reolink-aio==0.13.2"]
} }

View File

@ -70,7 +70,7 @@ class ReolinkVODMediaSource(MediaSource):
host = get_host(self.hass, config_entry_id) host = get_host(self.hass, config_entry_id)
def get_vod_type() -> VodRequestType: def get_vod_type() -> VodRequestType:
if filename.endswith((".mp4", ".vref")): if filename.endswith((".mp4", ".vref")) or host.api.is_hub:
if host.api.is_nvr: if host.api.is_nvr:
return VodRequestType.DOWNLOAD return VodRequestType.DOWNLOAD
return VodRequestType.PLAYBACK return VodRequestType.PLAYBACK

View File

@ -79,11 +79,15 @@ def get_device_uid_and_ch(
device: dr.DeviceEntry, host: ReolinkHost device: dr.DeviceEntry, host: ReolinkHost
) -> tuple[list[str], int | None, bool]: ) -> tuple[list[str], int | None, bool]:
"""Get the channel and the split device_uid from a reolink DeviceEntry.""" """Get the channel and the split device_uid from a reolink DeviceEntry."""
device_uid = [ device_uid = []
dev_id[1].split("_") for dev_id in device.identifiers if dev_id[0] == DOMAIN
][0]
is_chime = False is_chime = False
for dev_id in device.identifiers:
if dev_id[0] == DOMAIN:
device_uid = dev_id[1].split("_")
if device_uid[0] == host.unique_id:
break
if len(device_uid) < 2: if len(device_uid) < 2:
# NVR itself # NVR itself
ch = None ch = None

View File

@ -209,7 +209,7 @@ KELVIN_MIN_VALUE_COLOR: Final = 3000
BLOCK_WRONG_SLEEP_PERIOD = 21600 BLOCK_WRONG_SLEEP_PERIOD = 21600
BLOCK_EXPECTED_SLEEP_PERIOD = 43200 BLOCK_EXPECTED_SLEEP_PERIOD = 43200
UPTIME_DEVIATION: Final = 5 UPTIME_DEVIATION: Final = 60
# Time to wait before reloading entry upon device config change # Time to wait before reloading entry upon device config change
ENTRY_RELOAD_COOLDOWN = 60 ENTRY_RELOAD_COOLDOWN = 60

View File

@ -200,8 +200,18 @@ def get_device_uptime(uptime: float, last_uptime: datetime | None) -> datetime:
if ( if (
not last_uptime not last_uptime
or abs((delta_uptime - last_uptime).total_seconds()) > UPTIME_DEVIATION or (diff := abs((delta_uptime - last_uptime).total_seconds()))
> UPTIME_DEVIATION
): ):
if last_uptime:
LOGGER.debug(
"Time deviation %s > %s: uptime=%s, last_uptime=%s, delta_uptime=%s",
diff,
UPTIME_DEVIATION,
uptime,
last_uptime,
delta_uptime,
)
return delta_uptime return delta_uptime
return last_uptime return last_uptime

View File

@ -59,10 +59,11 @@ CAPABILITY_TO_SENSORS: dict[
Category.DOOR: BinarySensorDeviceClass.DOOR, Category.DOOR: BinarySensorDeviceClass.DOOR,
Category.WINDOW: BinarySensorDeviceClass.WINDOW, Category.WINDOW: BinarySensorDeviceClass.WINDOW,
}, },
exists_fn=lambda key: key in {"freezer", "cooler"}, exists_fn=lambda key: key in {"freezer", "cooler", "cvroom"},
component_translation_key={ component_translation_key={
"freezer": "freezer_door", "freezer": "freezer_door",
"cooler": "cooler_door", "cooler": "cooler_door",
"cvroom": "cool_select_plus_door",
}, },
deprecated_fn=( deprecated_fn=(
lambda status: "fridge_door" lambda status: "fridge_door"

View File

@ -23,7 +23,6 @@ from .entity import SmartThingsEntity
MEDIA_PLAYER_CAPABILITIES = ( MEDIA_PLAYER_CAPABILITIES = (
Capability.AUDIO_MUTE, Capability.AUDIO_MUTE,
Capability.AUDIO_VOLUME, Capability.AUDIO_VOLUME,
Capability.MEDIA_PLAYBACK,
) )
CONTROLLABLE_SOURCES = ["bluetooth", "wifi"] CONTROLLABLE_SOURCES = ["bluetooth", "wifi"]
@ -100,27 +99,25 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
) )
def _determine_features(self) -> MediaPlayerEntityFeature: def _determine_features(self) -> MediaPlayerEntityFeature:
flags = MediaPlayerEntityFeature(0) flags = (
playback_commands = self.get_attribute_value( MediaPlayerEntityFeature.VOLUME_SET
Capability.MEDIA_PLAYBACK, Attribute.SUPPORTED_PLAYBACK_COMMANDS | MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
) )
if "play" in playback_commands: if self.supports_capability(Capability.MEDIA_PLAYBACK):
flags |= MediaPlayerEntityFeature.PLAY playback_commands = self.get_attribute_value(
if "pause" in playback_commands: Capability.MEDIA_PLAYBACK, Attribute.SUPPORTED_PLAYBACK_COMMANDS
flags |= MediaPlayerEntityFeature.PAUSE
if "stop" in playback_commands:
flags |= MediaPlayerEntityFeature.STOP
if "rewind" in playback_commands:
flags |= MediaPlayerEntityFeature.PREVIOUS_TRACK
if "fastForward" in playback_commands:
flags |= MediaPlayerEntityFeature.NEXT_TRACK
if self.supports_capability(Capability.AUDIO_VOLUME):
flags |= (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP
) )
if self.supports_capability(Capability.AUDIO_MUTE): if "play" in playback_commands:
flags |= MediaPlayerEntityFeature.VOLUME_MUTE flags |= MediaPlayerEntityFeature.PLAY
if "pause" in playback_commands:
flags |= MediaPlayerEntityFeature.PAUSE
if "stop" in playback_commands:
flags |= MediaPlayerEntityFeature.STOP
if "rewind" in playback_commands:
flags |= MediaPlayerEntityFeature.PREVIOUS_TRACK
if "fastForward" in playback_commands:
flags |= MediaPlayerEntityFeature.NEXT_TRACK
if self.supports_capability(Capability.SWITCH): if self.supports_capability(Capability.SWITCH):
flags |= ( flags |= (
MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF
@ -270,6 +267,13 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
def state(self) -> MediaPlayerState | None: def state(self) -> MediaPlayerState | None:
"""State of the media player.""" """State of the media player."""
if self.supports_capability(Capability.SWITCH): if self.supports_capability(Capability.SWITCH):
if not self.supports_capability(Capability.MEDIA_PLAYBACK):
if (
self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH)
== "on"
):
return MediaPlayerState.ON
return MediaPlayerState.OFF
if self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on": if self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on":
if ( if (
self.source is not None self.source is not None

View File

@ -194,13 +194,7 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
deprecated=( deprecated=(
lambda status: "media_player" lambda status: "media_player"
if all( if Capability.AUDIO_MUTE in status
capability in status
for capability in (
Capability.AUDIO_MUTE,
Capability.MEDIA_PLAYBACK,
)
)
else None else None
), ),
) )

View File

@ -48,6 +48,9 @@
"cooler_door": { "cooler_door": {
"name": "Cooler door" "name": "Cooler door"
}, },
"cool_select_plus_door": {
"name": "CoolSelect+ door"
},
"remote_control": { "remote_control": {
"name": "Remote control" "name": "Remote control"
}, },

View File

@ -38,7 +38,6 @@ AC_CAPABILITIES = (
MEDIA_PLAYER_CAPABILITIES = ( MEDIA_PLAYER_CAPABILITIES = (
Capability.AUDIO_MUTE, Capability.AUDIO_MUTE,
Capability.AUDIO_VOLUME, Capability.AUDIO_VOLUME,
Capability.MEDIA_PLAYBACK,
) )

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/smhi", "documentation": "https://www.home-assistant.io/integrations/smhi",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pysmhi"], "loggers": ["pysmhi"],
"requirements": ["pysmhi==1.0.1"] "requirements": ["pysmhi==1.0.2"]
} }

View File

@ -148,7 +148,7 @@ def _build_response_apps_radios_category(
) -> BrowseMedia: ) -> BrowseMedia:
"""Build item for App or radio category.""" """Build item for App or radio category."""
return BrowseMedia( return BrowseMedia(
media_content_id=item.get("id", ""), media_content_id=item["id"],
title=item["title"], title=item["title"],
media_content_type=cmd, media_content_type=cmd,
media_class=browse_data.content_type_media_class[cmd]["item"], media_class=browse_data.content_type_media_class[cmd]["item"],
@ -163,7 +163,7 @@ def _build_response_known_app(
"""Build item for app or radio.""" """Build item for app or radio."""
return BrowseMedia( return BrowseMedia(
media_content_id=item.get("id", ""), media_content_id=item["id"],
title=item["title"], title=item["title"],
media_content_type=search_type, media_content_type=search_type,
media_class=browse_data.content_type_media_class[search_type]["item"], media_class=browse_data.content_type_media_class[search_type]["item"],
@ -185,7 +185,7 @@ def _build_response_favorites(item: dict[str, Any]) -> BrowseMedia:
) )
if item["hasitems"] and not item["isaudio"]: if item["hasitems"] and not item["isaudio"]:
return BrowseMedia( return BrowseMedia(
media_content_id=item.get("id", ""), media_content_id=item["id"],
title=item["title"], title=item["title"],
media_content_type="Favorites", media_content_type="Favorites",
media_class=CONTENT_TYPE_MEDIA_CLASS["Favorites"]["item"], media_class=CONTENT_TYPE_MEDIA_CLASS["Favorites"]["item"],
@ -193,7 +193,7 @@ def _build_response_favorites(item: dict[str, Any]) -> BrowseMedia:
can_play=False, can_play=False,
) )
return BrowseMedia( return BrowseMedia(
media_content_id=item.get("id", ""), media_content_id=item["id"],
title=item["title"], title=item["title"],
media_content_type="Favorites", media_content_type="Favorites",
media_class=CONTENT_TYPE_MEDIA_CLASS[MediaType.TRACK]["item"], media_class=CONTENT_TYPE_MEDIA_CLASS[MediaType.TRACK]["item"],
@ -217,7 +217,7 @@ def _get_item_thumbnail(
item_thumbnail = player.generate_image_url_from_track_id(artwork_track_id) item_thumbnail = player.generate_image_url_from_track_id(artwork_track_id)
elif item_type is not None: elif item_type is not None:
item_thumbnail = entity.get_browse_image_url( item_thumbnail = entity.get_browse_image_url(
item_type, item.get("id", ""), artwork_track_id item_type, item["id"], artwork_track_id
) )
elif search_type in ["Apps", "Radios"]: elif search_type in ["Apps", "Radios"]:
@ -263,6 +263,8 @@ async def build_item_response(
children = [] children = []
for item in result["items"]: for item in result["items"]:
# Force the item id to a string in case it's numeric from some lms
item["id"] = str(item.get("id", ""))
if search_type == "Favorites": if search_type == "Favorites":
child_media = _build_response_favorites(item) child_media = _build_response_favorites(item)
@ -294,7 +296,7 @@ async def build_item_response(
elif item_type: elif item_type:
child_media = BrowseMedia( child_media = BrowseMedia(
media_content_id=str(item.get("id", "")), media_content_id=item["id"],
title=item["title"], title=item["title"],
media_content_type=item_type, media_content_type=item_type,
media_class=CONTENT_TYPE_MEDIA_CLASS[item_type]["item"], media_class=CONTENT_TYPE_MEDIA_CLASS[item_type]["item"],

View File

@ -113,7 +113,9 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = (
translation_key="last_boot_time", translation_key="last_boot_time",
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: now() - timedelta(seconds=data.status["uptime"]), value_fn=lambda data: (
now() - timedelta(seconds=data.status["uptime"])
).replace(microsecond=0),
), ),
StarlinkSensorEntityDescription( StarlinkSensorEntityDescription(
key="ping_drop_rate", key="ping_drop_rate",

View File

@ -6,7 +6,12 @@ import logging
from aiohttp import ClientError, ClientResponseError from aiohttp import ClientError, ClientResponseError
from tesla_fleet_api.const import Scope from tesla_fleet_api.const import Scope
from tesla_fleet_api.exceptions import TeslaFleetError from tesla_fleet_api.exceptions import (
Forbidden,
InvalidToken,
SubscriptionRequired,
TeslaFleetError,
)
from tesla_fleet_api.tessie import Tessie from tesla_fleet_api.tessie import Tessie
from tessie_api import get_state_of_all_vehicles from tessie_api import get_state_of_all_vehicles
@ -124,12 +129,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
continue continue
api = tessie.energySites.create(site_id) api = tessie.energySites.create(site_id)
try:
live_status = (await api.live_status())["response"]
except (InvalidToken, Forbidden, SubscriptionRequired) as e:
raise ConfigEntryAuthFailed from e
except TeslaFleetError as e:
raise ConfigEntryNotReady(e.message) from e
energysites.append( energysites.append(
TessieEnergyData( TessieEnergyData(
api=api, api=api,
id=site_id, id=site_id,
live_coordinator=TessieEnergySiteLiveCoordinator( live_coordinator=(
hass, entry, api TessieEnergySiteLiveCoordinator(
hass, entry, api, live_status
)
if isinstance(live_status, dict)
else None
), ),
info_coordinator=TessieEnergySiteInfoCoordinator( info_coordinator=TessieEnergySiteInfoCoordinator(
hass, entry, api hass, entry, api
@ -147,6 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
*( *(
energysite.live_coordinator.async_config_entry_first_refresh() energysite.live_coordinator.async_config_entry_first_refresh()
for energysite in energysites for energysite in energysites
if energysite.live_coordinator is not None
), ),
*( *(
energysite.info_coordinator.async_config_entry_first_refresh() energysite.info_coordinator.async_config_entry_first_refresh()

View File

@ -191,6 +191,7 @@ async def async_setup_entry(
TessieEnergyLiveBinarySensorEntity(energy, description) TessieEnergyLiveBinarySensorEntity(energy, description)
for energy in entry.runtime_data.energysites for energy in entry.runtime_data.energysites
for description in ENERGY_LIVE_DESCRIPTIONS for description in ENERGY_LIVE_DESCRIPTIONS
if energy.live_coordinator is not None
), ),
( (
TessieEnergyInfoBinarySensorEntity(vehicle, description) TessieEnergyInfoBinarySensorEntity(vehicle, description)
@ -233,6 +234,7 @@ class TessieEnergyLiveBinarySensorEntity(TessieEnergyEntity, BinarySensorEntity)
) -> None: ) -> None:
"""Initialize the binary sensor.""" """Initialize the binary sensor."""
self.entity_description = description self.entity_description = description
assert data.live_coordinator is not None
super().__init__(data, data.live_coordinator, description.key) super().__init__(data, data.live_coordinator, description.key)
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> None:

View File

@ -102,7 +102,11 @@ class TessieEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]):
config_entry: TessieConfigEntry config_entry: TessieConfigEntry
def __init__( def __init__(
self, hass: HomeAssistant, config_entry: TessieConfigEntry, api: EnergySite self,
hass: HomeAssistant,
config_entry: TessieConfigEntry,
api: EnergySite,
data: dict[str, Any],
) -> None: ) -> None:
"""Initialize Tessie Energy Site Live coordinator.""" """Initialize Tessie Energy Site Live coordinator."""
super().__init__( super().__init__(
@ -114,6 +118,12 @@ class TessieEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]):
) )
self.api = api self.api = api
# Convert Wall Connectors from array to dict
data["wall_connectors"] = {
wc["din"]: wc for wc in (data.get("wall_connectors") or [])
}
self.data = data
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Update energy site data using Tessie API.""" """Update energy site data using Tessie API."""

View File

@ -41,7 +41,9 @@ async def async_get_config_entry_diagnostics(
] ]
energysites = [ energysites = [
{ {
"live": async_redact_data(x.live_coordinator.data, ENERGY_LIVE_REDACT), "live": async_redact_data(x.live_coordinator.data, ENERGY_LIVE_REDACT)
if x.live_coordinator
else None,
"info": async_redact_data(x.info_coordinator.data, ENERGY_INFO_REDACT), "info": async_redact_data(x.info_coordinator.data, ENERGY_INFO_REDACT),
} }
for x in entry.runtime_data.energysites for x in entry.runtime_data.energysites

View File

@ -155,7 +155,7 @@ class TessieWallConnectorEntity(TessieBaseEntity):
via_device=(DOMAIN, str(data.id)), via_device=(DOMAIN, str(data.id)),
serial_number=din.split("-")[-1], serial_number=din.split("-")[-1],
) )
assert data.live_coordinator
super().__init__(data.live_coordinator, key) super().__init__(data.live_coordinator, key)
@property @property

View File

@ -28,7 +28,7 @@ class TessieEnergyData:
"""Data for a Energy Site in the Tessie integration.""" """Data for a Energy Site in the Tessie integration."""
api: EnergySite api: EnergySite
live_coordinator: TessieEnergySiteLiveCoordinator live_coordinator: TessieEnergySiteLiveCoordinator | None
info_coordinator: TessieEnergySiteInfoCoordinator info_coordinator: TessieEnergySiteInfoCoordinator
id: int id: int
device: DeviceInfo device: DeviceInfo

View File

@ -396,12 +396,16 @@ async def async_setup_entry(
TessieEnergyLiveSensorEntity(energysite, description) TessieEnergyLiveSensorEntity(energysite, description)
for energysite in entry.runtime_data.energysites for energysite in entry.runtime_data.energysites
for description in ENERGY_LIVE_DESCRIPTIONS for description in ENERGY_LIVE_DESCRIPTIONS
if description.key in energysite.live_coordinator.data if energysite.live_coordinator is not None
or description.key == "percentage_charged" and (
description.key in energysite.live_coordinator.data
or description.key == "percentage_charged"
)
), ),
( # Add wall connectors ( # Add wall connectors
TessieWallConnectorSensorEntity(energysite, din, description) TessieWallConnectorSensorEntity(energysite, din, description)
for energysite in entry.runtime_data.energysites for energysite in entry.runtime_data.energysites
if energysite.live_coordinator is not None
for din in energysite.live_coordinator.data.get("wall_connectors", {}) for din in energysite.live_coordinator.data.get("wall_connectors", {})
for description in WALL_CONNECTOR_DESCRIPTIONS for description in WALL_CONNECTOR_DESCRIPTIONS
), ),
@ -446,6 +450,7 @@ class TessieEnergyLiveSensorEntity(TessieEnergyEntity, SensorEntity):
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
self.entity_description = description self.entity_description = description
assert data.live_coordinator is not None
super().__init__(data, data.live_coordinator, description.key) super().__init__(data, data.live_coordinator, description.key)
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> None:

View File

@ -163,7 +163,7 @@ class UkTransportLiveBusTimeSensor(UkTransportSensor):
self._destination_re = re.compile(f"{bus_direction}", re.IGNORECASE) self._destination_re = re.compile(f"{bus_direction}", re.IGNORECASE)
sensor_name = f"Next bus to {bus_direction}" sensor_name = f"Next bus to {bus_direction}"
stop_url = f"bus/stop/{stop_atcocode}/live.json" stop_url = f"bus/stop/{stop_atcocode}.json"
UkTransportSensor.__init__(self, sensor_name, api_app_id, api_app_key, stop_url) UkTransportSensor.__init__(self, sensor_name, api_app_id, api_app_key, stop_url)
self.update = Throttle(interval)(self._update) self.update = Throttle(interval)(self._update)
@ -226,7 +226,7 @@ class UkTransportLiveTrainTimeSensor(UkTransportSensor):
self._next_trains = [] self._next_trains = []
sensor_name = f"Next train to {calling_at}" sensor_name = f"Next train to {calling_at}"
query_url = f"train/station/{station_code}/live.json" query_url = f"train/station/{station_code}.json"
UkTransportSensor.__init__( UkTransportSensor.__init__(
self, sensor_name, api_app_id, api_app_key, query_url self, sensor_name, api_app_id, api_app_key, query_url

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["holidays"], "loggers": ["holidays"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["holidays==0.69"] "requirements": ["holidays==0.70"]
} }

View File

@ -21,7 +21,7 @@
"zha", "zha",
"universal_silabs_flasher" "universal_silabs_flasher"
], ],
"requirements": ["zha==0.0.55"], "requirements": ["zha==0.0.56"],
"usb": [ "usb": [
{ {
"vid": "10C4", "vid": "10C4",

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025 MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 4 MINOR_VERSION: Final = 4
PATCH_VERSION: Final = "2" PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0)

View File

@ -71,6 +71,19 @@ NO_ENTITIES_PROMPT = (
"to their voice assistant in Home Assistant." "to their voice assistant in Home Assistant."
) )
DYNAMIC_CONTEXT_PROMPT = """You ARE equipped to answer questions about the current state of
the home using the `GetLiveContext` tool. This is a primary function. Do not state you lack the
functionality if the question requires live data.
If the user asks about device existence/type (e.g., "Do I have lights in the bedroom?"): Answer
from the static context below.
If the user asks about the CURRENT state, value, or mode (e.g., "Is the lock locked?",
"Is the fan on?", "What mode is the thermostat in?", "What is the temperature outside?"):
1. Recognize this requires live data.
2. You MUST call `GetLiveContext`. This tool will provide the needed real-time information (like temperature from the local weather, lock status, etc.).
3. Use the tool's response** to answer the user accurately (e.g., "The temperature outside is [value from tool].").
For general knowledge questions not about the home: Answer truthfully from internal knowledge.
"""
@callback @callback
def async_render_no_api_prompt(hass: HomeAssistant) -> str: def async_render_no_api_prompt(hass: HomeAssistant) -> str:
@ -384,6 +397,8 @@ class AssistAPI(API):
): ):
prompt.append("This device is not able to start timers.") prompt.append("This device is not able to start timers.")
prompt.append(DYNAMIC_CONTEXT_PROMPT)
return prompt return prompt
@callback @callback
@ -395,7 +410,7 @@ class AssistAPI(API):
if exposed_entities and exposed_entities["entities"]: if exposed_entities and exposed_entities["entities"]:
prompt.append( prompt.append(
"An overview of the areas and the devices in this smart home:" "Static Context: An overview of the areas and the devices in this smart home:"
) )
prompt.append(yaml_util.dump(list(exposed_entities["entities"].values()))) prompt.append(yaml_util.dump(list(exposed_entities["entities"].values())))
@ -457,7 +472,7 @@ class AssistAPI(API):
) )
if exposed_domains: if exposed_domains:
tools.append(GetHomeStateTool()) tools.append(GetLiveContextTool())
return tools return tools
@ -898,7 +913,7 @@ class CalendarGetEventsTool(Tool):
return {"success": True, "result": events} return {"success": True, "result": events}
class GetHomeStateTool(Tool): class GetLiveContextTool(Tool):
"""Tool for getting the current state of exposed entities. """Tool for getting the current state of exposed entities.
This returns state for all entities that have been exposed to This returns state for all entities that have been exposed to
@ -906,8 +921,13 @@ class GetHomeStateTool(Tool):
returns state for entities based on intent parameters. returns state for entities based on intent parameters.
""" """
name = "get_home_state" name = "GetLiveContext"
description = "Get the current state of all devices in the home. " description = (
"Use this tool when the user asks a question about the CURRENT state, "
"value, or mode of a specific device, sensor, entity, or area in the "
"smart home, and the answer can be improved with real-time data not "
"available in the static device overview list. "
)
async def async_call( async def async_call(
self, self,
@ -925,7 +945,7 @@ class GetHomeStateTool(Tool):
if not exposed_entities["entities"]: if not exposed_entities["entities"]:
return {"success": False, "error": NO_ENTITIES_PROMPT} return {"success": False, "error": NO_ENTITIES_PROMPT}
prompt = [ prompt = [
"An overview of the areas and the devices in this smart home:", "Live Context: An overview of the areas and the devices in this smart home:",
yaml_util.dump(list(exposed_entities["entities"].values())), yaml_util.dump(list(exposed_entities["entities"].values())),
] ]
return { return {

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2025.4.2" version = "2025.4.3"
license = "Apache-2.0" license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."

12
requirements_all.txt generated
View File

@ -781,7 +781,7 @@ devialet==1.5.7
devolo-home-control-api==0.18.3 devolo-home-control-api==0.18.3
# homeassistant.components.devolo_home_network # homeassistant.components.devolo_home_network
devolo-plc-api==1.4.1 devolo-plc-api==1.5.1
# homeassistant.components.chacon_dio # homeassistant.components.chacon_dio
dio-chacon-wifi-api==1.2.1 dio-chacon-wifi-api==1.2.1
@ -871,7 +871,7 @@ enocean==0.50
enturclient==0.2.4 enturclient==0.2.4
# homeassistant.components.environment_canada # homeassistant.components.environment_canada
env-canada==0.8.0 env-canada==0.10.1
# homeassistant.components.season # homeassistant.components.season
ephem==4.1.6 ephem==4.1.6
@ -1154,7 +1154,7 @@ hole==0.8.0
# homeassistant.components.holiday # homeassistant.components.holiday
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.69 holidays==0.70
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20250411.0 home-assistant-frontend==20250411.0
@ -2325,7 +2325,7 @@ pysmartthings==3.0.4
pysmarty2==0.10.2 pysmarty2==0.10.2
# homeassistant.components.smhi # homeassistant.components.smhi
pysmhi==1.0.1 pysmhi==1.0.2
# homeassistant.components.edl21 # homeassistant.components.edl21
pysml==0.0.12 pysml==0.0.12
@ -2627,7 +2627,7 @@ renault-api==0.2.9
renson-endura-delta==1.7.2 renson-endura-delta==1.7.2
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.13.1 reolink-aio==0.13.2
# homeassistant.components.idteck_prox # homeassistant.components.idteck_prox
rfk101py==0.0.1 rfk101py==0.0.1
@ -3152,7 +3152,7 @@ zeroconf==0.146.0
zeversolar==0.3.2 zeversolar==0.3.2
# homeassistant.components.zha # homeassistant.components.zha
zha==0.0.55 zha==0.0.56
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13 zhong-hong-hvac==1.0.13

View File

@ -672,7 +672,7 @@ devialet==1.5.7
devolo-home-control-api==0.18.3 devolo-home-control-api==0.18.3
# homeassistant.components.devolo_home_network # homeassistant.components.devolo_home_network
devolo-plc-api==1.4.1 devolo-plc-api==1.5.1
# homeassistant.components.chacon_dio # homeassistant.components.chacon_dio
dio-chacon-wifi-api==1.2.1 dio-chacon-wifi-api==1.2.1
@ -741,7 +741,7 @@ energyzero==2.1.1
enocean==0.50 enocean==0.50
# homeassistant.components.environment_canada # homeassistant.components.environment_canada
env-canada==0.8.0 env-canada==0.10.1
# homeassistant.components.season # homeassistant.components.season
ephem==4.1.6 ephem==4.1.6
@ -981,7 +981,7 @@ hole==0.8.0
# homeassistant.components.holiday # homeassistant.components.holiday
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.69 holidays==0.70
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20250411.0 home-assistant-frontend==20250411.0
@ -1895,7 +1895,7 @@ pysmartthings==3.0.4
pysmarty2==0.10.2 pysmarty2==0.10.2
# homeassistant.components.smhi # homeassistant.components.smhi
pysmhi==1.0.1 pysmhi==1.0.2
# homeassistant.components.edl21 # homeassistant.components.edl21
pysml==0.0.12 pysml==0.0.12
@ -2128,7 +2128,7 @@ renault-api==0.2.9
renson-endura-delta==1.7.2 renson-endura-delta==1.7.2
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.13.1 reolink-aio==0.13.2
# homeassistant.components.rflink # homeassistant.components.rflink
rflink==0.0.66 rflink==0.0.66
@ -2542,7 +2542,7 @@ zeroconf==0.146.0
zeversolar==0.3.2 zeversolar==0.3.2
# homeassistant.components.zha # homeassistant.components.zha
zha==0.0.55 zha==0.0.56
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.62.0 zwave-js-server-python==0.62.0

View File

@ -88,6 +88,7 @@ OSI_APPROVED_LICENSES_SPDX = {
"MPL-1.1", "MPL-1.1",
"MPL-2.0", "MPL-2.0",
"PSF-2.0", "PSF-2.0",
"Python-2.0",
"Unlicense", "Unlicense",
"Zlib", "Zlib",
"ZPL-2.1", "ZPL-2.1",

View File

@ -33,7 +33,7 @@ async def init_integration(hass: HomeAssistant, ec_data) -> MockConfigEntry:
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
weather_mock = mock_ec() weather_mock = mock_ec()
ec_data["metadata"]["timestamp"] = datetime(2022, 10, 4, tzinfo=UTC) ec_data["metadata"].timestamp = datetime(2022, 10, 4, tzinfo=UTC)
weather_mock.conditions = ec_data["conditions"] weather_mock.conditions = ec_data["conditions"]
weather_mock.alerts = ec_data["alerts"] weather_mock.alerts = ec_data["alerts"]
weather_mock.daily_forecasts = ec_data["daily_forecasts"] weather_mock.daily_forecasts = ec_data["daily_forecasts"]

View File

@ -4,6 +4,7 @@ import contextlib
from datetime import datetime from datetime import datetime
import json import json
from env_canada.ec_weather import MetaData
import pytest import pytest
from tests.common import load_fixture from tests.common import load_fixture
@ -13,7 +14,7 @@ from tests.common import load_fixture
def ec_data(): def ec_data():
"""Load Environment Canada data.""" """Load Environment Canada data."""
def date_hook(weather): def data_hook(weather):
"""Convert timestamp string to datetime.""" """Convert timestamp string to datetime."""
if t := weather.get("timestamp"): if t := weather.get("timestamp"):
@ -22,9 +23,11 @@ def ec_data():
elif t := weather.get("period"): elif t := weather.get("period"):
with contextlib.suppress(ValueError): with contextlib.suppress(ValueError):
weather["period"] = datetime.fromisoformat(t) weather["period"] = datetime.fromisoformat(t)
if t := weather.get("metadata"):
weather["metadata"] = MetaData(**t)
return weather return weather
return json.loads( return json.loads(
load_fixture("environment_canada/current_conditions_data.json"), load_fixture("environment_canada/current_conditions_data.json"),
object_hook=date_hook, object_hook=data_hook,
) )

View File

@ -30,7 +30,7 @@ def mocked_ec():
ec_mock.lat = FAKE_CONFIG[CONF_LATITUDE] ec_mock.lat = FAKE_CONFIG[CONF_LATITUDE]
ec_mock.lon = FAKE_CONFIG[CONF_LONGITUDE] ec_mock.lon = FAKE_CONFIG[CONF_LONGITUDE]
ec_mock.language = FAKE_CONFIG[CONF_LANGUAGE] ec_mock.language = FAKE_CONFIG[CONF_LANGUAGE]
ec_mock.metadata = {"location": FAKE_TITLE} ec_mock.metadata.location = FAKE_TITLE
ec_mock.update = AsyncMock() ec_mock.update = AsyncMock()

View File

@ -1,6 +1,5 @@
"""Test Environment Canada diagnostics.""" """Test Environment Canada diagnostics."""
import json
from typing import Any from typing import Any
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -11,7 +10,6 @@ from homeassistant.core import HomeAssistant
from . import init_integration from . import init_integration
from tests.common import load_fixture
from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator from tests.typing import ClientSessionGenerator
@ -31,10 +29,6 @@ async def test_entry_diagnostics(
) -> None: ) -> None:
"""Test config entry diagnostics.""" """Test config entry diagnostics."""
ec_data = json.loads(
load_fixture("environment_canada/current_conditions_data.json")
)
config_entry = await init_integration(hass, ec_data) config_entry = await init_integration(hass, ec_data)
diagnostics = await get_diagnostics_for_config_entry( diagnostics = await get_diagnostics_for_config_entry(
hass, hass_client, config_entry hass, hass_client, config_entry

View File

@ -624,6 +624,49 @@
"isDue": false, "isDue": false,
"id": "6e53f1f5-a315-4edd-984d-8d762e4a08ef" "id": "6e53f1f5-a315-4edd-984d-8d762e4a08ef"
}, },
{
"repeat": {
"m": false,
"t": false,
"w": false,
"th": false,
"f": false,
"s": false,
"su": true
},
"challenge": {},
"group": {
"completedBy": {},
"assignedUsers": []
},
"_id": "369afeed-61e3-4bf7-9747-66e05807134c",
"frequency": "monthly",
"everyX": 1,
"streak": 1,
"nextDue": ["2024-12-14T23:00:00.000Z", "2025-01-18T23:00:00.000Z"],
"yesterDaily": true,
"history": [],
"completed": false,
"collapseChecklist": false,
"type": "daily",
"text": "Monatliche Finanzübersicht erstellen",
"notes": "Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.",
"tags": [],
"value": -0.9215181434950852,
"priority": 1,
"attribute": "str",
"byHabitica": false,
"startDate": "2024-04-04T22:00:00.000Z",
"daysOfMonth": [],
"weeksOfMonth": [0],
"checklist": [],
"reminders": [],
"createdAt": "2024-04-04T22:00:00.000Z",
"updatedAt": "2024-04-04T22:00:00.000Z",
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
"isDue": false,
"id": "369afeed-61e3-4bf7-9747-66e05807134c"
},
{ {
"repeat": { "repeat": {
"m": false, "m": false,

View File

@ -66,7 +66,8 @@
"564b9ac9-c53d-4638-9e7f-1cd96fe19baa", "564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
"f2c85972-1a19-4426-bc6d-ce3337b9d99f", "f2c85972-1a19-4426-bc6d-ce3337b9d99f",
"2c6d136c-a1c3-4bef-b7c4-fa980784b1e1", "2c6d136c-a1c3-4bef-b7c4-fa980784b1e1",
"6e53f1f5-a315-4edd-984d-8d762e4a08ef" "6e53f1f5-a315-4edd-984d-8d762e4a08ef",
"369afeed-61e3-4bf7-9747-66e05807134c"
], ],
"habits": ["1d147de6-5c02-4740-8e2f-71d3015a37f4"] "habits": ["1d147de6-5c02-4740-8e2f-71d3015a37f4"]
}, },

View File

@ -87,6 +87,20 @@
'summary': 'Fitnessstudio besuchen', 'summary': 'Fitnessstudio besuchen',
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1', 'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
}), }),
dict({
'description': 'Klicke um den Namen Deines aktuellen Projekts anzugeben & setze einen Terminplan!',
'end': dict({
'date': '2024-09-23',
}),
'location': None,
'recurrence_id': None,
'rrule': 'FREQ=MONTHLY;BYSETPOS=4;BYDAY=SU',
'start': dict({
'date': '2024-09-22',
}),
'summary': 'Arbeite an einem kreativen Projekt',
'uid': '6e53f1f5-a315-4edd-984d-8d762e4a08ef',
}),
dict({ dict({
'description': 'Klicke um Änderungen zu machen!', 'description': 'Klicke um Änderungen zu machen!',
'end': dict({ 'end': dict({
@ -563,6 +577,20 @@
'summary': 'Fitnessstudio besuchen', 'summary': 'Fitnessstudio besuchen',
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1', 'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
}), }),
dict({
'description': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.',
'end': dict({
'date': '2024-10-07',
}),
'location': None,
'recurrence_id': None,
'rrule': 'FREQ=MONTHLY;BYSETPOS=1;BYDAY=SU',
'start': dict({
'date': '2024-10-06',
}),
'summary': 'Monatliche Finanzübersicht erstellen',
'uid': '369afeed-61e3-4bf7-9747-66e05807134c',
}),
dict({ dict({
'description': 'Klicke um Änderungen zu machen!', 'description': 'Klicke um Änderungen zu machen!',
'end': dict({ 'end': dict({

View File

@ -1193,6 +1193,81 @@
]), ]),
'yesterDaily': True, 'yesterDaily': True,
}), }),
dict({
'alias': None,
'attribute': 'str',
'byHabitica': False,
'challenge': dict({
'broken': None,
'id': None,
'shortName': None,
'taskId': None,
'winner': None,
}),
'checklist': list([
]),
'collapseChecklist': False,
'completed': False,
'counterDown': None,
'counterUp': None,
'createdAt': '2024-04-04T22:00:00+00:00',
'date': None,
'daysOfMonth': list([
]),
'down': None,
'everyX': 1,
'frequency': 'monthly',
'group': dict({
'assignedDate': None,
'assignedUsers': list([
]),
'assignedUsersDetail': dict({
}),
'assigningUsername': None,
'completedBy': dict({
'date': None,
'userId': None,
}),
'id': None,
'managerNotes': None,
'taskId': None,
}),
'history': list([
]),
'id': '369afeed-61e3-4bf7-9747-66e05807134c',
'isDue': False,
'nextDue': list([
'2024-12-14T23:00:00+00:00',
'2025-01-18T23:00:00+00:00',
]),
'notes': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.',
'priority': 1,
'reminders': list([
]),
'repeat': dict({
'f': False,
'm': False,
's': False,
'su': True,
't': False,
'th': False,
'w': False,
}),
'startDate': '2024-04-04T22:00:00+00:00',
'streak': 1,
'tags': list([
]),
'text': 'Monatliche Finanzübersicht erstellen',
'type': 'daily',
'up': None,
'updatedAt': '2024-04-04T22:00:00+00:00',
'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c',
'value': -0.9215181434950852,
'weeksOfMonth': list([
0,
]),
'yesterDaily': True,
}),
dict({ dict({
'alias': None, 'alias': None,
'attribute': 'str', 'attribute': 'str',
@ -3465,6 +3540,81 @@
]), ]),
'yesterDaily': True, 'yesterDaily': True,
}), }),
dict({
'alias': None,
'attribute': 'str',
'byHabitica': False,
'challenge': dict({
'broken': None,
'id': None,
'shortName': None,
'taskId': None,
'winner': None,
}),
'checklist': list([
]),
'collapseChecklist': False,
'completed': False,
'counterDown': None,
'counterUp': None,
'createdAt': '2024-04-04T22:00:00+00:00',
'date': None,
'daysOfMonth': list([
]),
'down': None,
'everyX': 1,
'frequency': 'monthly',
'group': dict({
'assignedDate': None,
'assignedUsers': list([
]),
'assignedUsersDetail': dict({
}),
'assigningUsername': None,
'completedBy': dict({
'date': None,
'userId': None,
}),
'id': None,
'managerNotes': None,
'taskId': None,
}),
'history': list([
]),
'id': '369afeed-61e3-4bf7-9747-66e05807134c',
'isDue': False,
'nextDue': list([
'2024-12-14T23:00:00+00:00',
'2025-01-18T23:00:00+00:00',
]),
'notes': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.',
'priority': 1,
'reminders': list([
]),
'repeat': dict({
'f': False,
'm': False,
's': False,
'su': True,
't': False,
'th': False,
'w': False,
}),
'startDate': '2024-04-04T22:00:00+00:00',
'streak': 1,
'tags': list([
]),
'text': 'Monatliche Finanzübersicht erstellen',
'type': 'daily',
'up': None,
'updatedAt': '2024-04-04T22:00:00+00:00',
'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c',
'value': -0.9215181434950852,
'weeksOfMonth': list([
0,
]),
'yesterDaily': True,
}),
dict({ dict({
'alias': None, 'alias': None,
'attribute': 'str', 'attribute': 'str',
@ -4608,6 +4758,81 @@
]), ]),
'yesterDaily': True, 'yesterDaily': True,
}), }),
dict({
'alias': None,
'attribute': 'str',
'byHabitica': False,
'challenge': dict({
'broken': None,
'id': None,
'shortName': None,
'taskId': None,
'winner': None,
}),
'checklist': list([
]),
'collapseChecklist': False,
'completed': False,
'counterDown': None,
'counterUp': None,
'createdAt': '2024-04-04T22:00:00+00:00',
'date': None,
'daysOfMonth': list([
]),
'down': None,
'everyX': 1,
'frequency': 'monthly',
'group': dict({
'assignedDate': None,
'assignedUsers': list([
]),
'assignedUsersDetail': dict({
}),
'assigningUsername': None,
'completedBy': dict({
'date': None,
'userId': None,
}),
'id': None,
'managerNotes': None,
'taskId': None,
}),
'history': list([
]),
'id': '369afeed-61e3-4bf7-9747-66e05807134c',
'isDue': False,
'nextDue': list([
'2024-12-14T23:00:00+00:00',
'2025-01-18T23:00:00+00:00',
]),
'notes': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.',
'priority': 1,
'reminders': list([
]),
'repeat': dict({
'f': False,
'm': False,
's': False,
'su': True,
't': False,
'th': False,
'w': False,
}),
'startDate': '2024-04-04T22:00:00+00:00',
'streak': 1,
'tags': list([
]),
'text': 'Monatliche Finanzübersicht erstellen',
'type': 'daily',
'up': None,
'updatedAt': '2024-04-04T22:00:00+00:00',
'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c',
'value': -0.9215181434950852,
'weeksOfMonth': list([
0,
]),
'yesterDaily': True,
}),
dict({ dict({
'alias': None, 'alias': None,
'attribute': 'str', 'attribute': 'str',
@ -5199,6 +5424,81 @@
]), ]),
'yesterDaily': True, 'yesterDaily': True,
}), }),
dict({
'alias': None,
'attribute': 'str',
'byHabitica': False,
'challenge': dict({
'broken': None,
'id': None,
'shortName': None,
'taskId': None,
'winner': None,
}),
'checklist': list([
]),
'collapseChecklist': False,
'completed': False,
'counterDown': None,
'counterUp': None,
'createdAt': '2024-04-04T22:00:00+00:00',
'date': None,
'daysOfMonth': list([
]),
'down': None,
'everyX': 1,
'frequency': 'monthly',
'group': dict({
'assignedDate': None,
'assignedUsers': list([
]),
'assignedUsersDetail': dict({
}),
'assigningUsername': None,
'completedBy': dict({
'date': None,
'userId': None,
}),
'id': None,
'managerNotes': None,
'taskId': None,
}),
'history': list([
]),
'id': '369afeed-61e3-4bf7-9747-66e05807134c',
'isDue': False,
'nextDue': list([
'2024-12-14T23:00:00+00:00',
'2025-01-18T23:00:00+00:00',
]),
'notes': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.',
'priority': 1,
'reminders': list([
]),
'repeat': dict({
'f': False,
'm': False,
's': False,
'su': True,
't': False,
'th': False,
'w': False,
}),
'startDate': '2024-04-04T22:00:00+00:00',
'streak': 1,
'tags': list([
]),
'text': 'Monatliche Finanzübersicht erstellen',
'type': 'daily',
'up': None,
'updatedAt': '2024-04-04T22:00:00+00:00',
'userId': '5f359083-ef78-4af0-985a-0b2c6d05797c',
'value': -0.9215181434950852,
'weeksOfMonth': list([
0,
]),
'yesterDaily': True,
}),
dict({ dict({
'alias': None, 'alias': None,
'attribute': 'str', 'attribute': 'str',

View File

@ -49,6 +49,13 @@
'summary': 'Arbeite an einem kreativen Projekt', 'summary': 'Arbeite an einem kreativen Projekt',
'uid': '6e53f1f5-a315-4edd-984d-8d762e4a08ef', 'uid': '6e53f1f5-a315-4edd-984d-8d762e4a08ef',
}), }),
dict({
'description': 'Setze dich einmal im Monat hin, um deine Einnahmen und Ausgaben zu überprüfen und dein Budget zu planen.',
'due': '2024-12-14',
'status': 'needs_action',
'summary': 'Monatliche Finanzübersicht erstellen',
'uid': '369afeed-61e3-4bf7-9747-66e05807134c',
}),
dict({ dict({
'description': 'Wähle eine Programmiersprache aus, die du noch nicht kennst, und lerne die Grundlagen.', 'description': 'Wähle eine Programmiersprache aus, die du noch nicht kennst, und lerne die Grundlagen.',
'status': 'needs_action', 'status': 'needs_action',
@ -151,7 +158,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '4', 'state': '5',
}) })
# --- # ---
# name: test_todos[todo.test_user_to_do_s-entry] # name: test_todos[todo.test_user_to_do_s-entry]

View File

@ -388,23 +388,181 @@ async def test_only_valid_components(
assert not mock_dispatcher_send.called assert not mock_dispatcher_send.called
async def test_correct_config_discovery( @pytest.mark.parametrize(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ("discovery_topic", "discovery_hash"),
[
("homeassistant/binary_sensor/bla/config", ("binary_sensor", "bla")),
("homeassistant/binary_sensor/node/bla/config", ("binary_sensor", "node bla")),
],
ids=["without_node", "with_node"],
)
async def test_correct_config_discovery_component(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
device_registry: dr.DeviceRegistry,
discovery_topic: str,
discovery_hash: tuple[str, str],
) -> None: ) -> None:
"""Test sending in correct JSON.""" """Test sending in correct JSON."""
await mqtt_mock_entry() await mqtt_mock_entry()
config_init = {
"name": "Beer",
"state_topic": "test-topic",
"unique_id": "bla001",
"device": {"identifiers": "0AFFD2", "name": "test_device1"},
"o": {"name": "foobar"},
}
async_fire_mqtt_message( async_fire_mqtt_message(
hass, hass,
"homeassistant/binary_sensor/bla/config", discovery_topic,
'{ "name": "Beer", "state_topic": "test-topic" }', json.dumps(config_init),
) )
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer") state = hass.states.get("binary_sensor.test_device1_beer")
assert state is not None assert state is not None
assert state.name == "Beer" assert state.name == "test_device1 Beer"
assert ("binary_sensor", "bla") in hass.data["mqtt"].discovery_already_discovered assert discovery_hash in hass.data["mqtt"].discovery_already_discovered
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is not None
assert device_entry.name == "test_device1"
# Update the device and component
config_update = {
"name": "Milk",
"state_topic": "test-topic",
"unique_id": "bla001",
"device": {"identifiers": "0AFFD2", "name": "test_device2"},
"o": {"name": "foobar"},
}
async_fire_mqtt_message(
hass,
discovery_topic,
json.dumps(config_update),
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test_device1_beer")
assert state is not None
assert state.name == "test_device2 Milk"
assert discovery_hash in hass.data["mqtt"].discovery_already_discovered
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is not None
assert device_entry.name == "test_device2"
# Remove the device and component
async_fire_mqtt_message(
hass,
discovery_topic,
"",
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test_device1_beer")
assert state is None
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is None
@pytest.mark.parametrize(
("discovery_topic", "discovery_hash"),
[
("homeassistant/device/some_id/config", ("binary_sensor", "some_id bla")),
(
"homeassistant/device/node_id/some_id/config",
("binary_sensor", "some_id node_id bla"),
),
],
ids=["without_node", "with_node"],
)
async def test_correct_config_discovery_device(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
device_registry: dr.DeviceRegistry,
discovery_topic: str,
discovery_hash: tuple[str, str],
) -> None:
"""Test sending in correct JSON."""
await mqtt_mock_entry()
config_init = {
"cmps": {
"bla": {
"platform": "binary_sensor",
"name": "Beer",
"state_topic": "test-topic",
"unique_id": "bla001",
},
},
"device": {"identifiers": "0AFFD2", "name": "test_device1"},
"o": {"name": "foobar"},
}
async_fire_mqtt_message(
hass,
discovery_topic,
json.dumps(config_init),
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test_device1_beer")
assert state is not None
assert state.name == "test_device1 Beer"
assert discovery_hash in hass.data["mqtt"].discovery_already_discovered
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is not None
assert device_entry.name == "test_device1"
# Update the device and component
config_update = {
"cmps": {
"bla": {
"platform": "binary_sensor",
"name": "Milk",
"state_topic": "test-topic",
"unique_id": "bla001",
},
},
"device": {"identifiers": "0AFFD2", "name": "test_device2"},
"o": {"name": "foobar"},
}
async_fire_mqtt_message(
hass,
discovery_topic,
json.dumps(config_update),
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test_device1_beer")
assert state is not None
assert state.name == "test_device2 Milk"
assert discovery_hash in hass.data["mqtt"].discovery_already_discovered
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is not None
assert device_entry.name == "test_device2"
# Remove the device and component
async_fire_mqtt_message(
hass,
discovery_topic,
"",
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test_device1_beer")
assert state is None
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is None
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -77,6 +77,7 @@ def reolink_connect_class() -> Generator[MagicMock]:
host_mock.check_new_firmware.return_value = False host_mock.check_new_firmware.return_value = False
host_mock.unsubscribe.return_value = True host_mock.unsubscribe.return_value = True
host_mock.logout.return_value = True host_mock.logout.return_value = True
host_mock.is_nvr = True
host_mock.is_hub = False host_mock.is_hub = False
host_mock.mac_address = TEST_MAC host_mock.mac_address = TEST_MAC
host_mock.uid = TEST_UID host_mock.uid = TEST_UID

View File

@ -23,15 +23,21 @@ from homeassistant.components.number import (
DOMAIN as NUMBER_DOMAIN, DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
) )
from homeassistant.components.reolink.const import DOMAIN
from homeassistant.components.reolink.util import get_device_uid_and_ch
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr
from .conftest import TEST_NVR_NAME from .conftest import TEST_NVR_NAME, TEST_UID, TEST_UID_CAM
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
DEV_ID_NVR = f"{TEST_UID}_{TEST_UID_CAM}"
DEV_ID_STANDALONE_CAM = f"{TEST_UID_CAM}"
@pytest.mark.parametrize( @pytest.mark.parametrize(
("side_effect", "expected"), ("side_effect", "expected"),
@ -123,3 +129,36 @@ async def test_try_function(
assert err.value.translation_key == expected.translation_key assert err.value.translation_key == expected.translation_key
reolink_connect.set_volume.reset_mock(side_effect=True) reolink_connect.set_volume.reset_mock(side_effect=True)
@pytest.mark.parametrize(
("identifiers"),
[
({(DOMAIN, DEV_ID_NVR), (DOMAIN, DEV_ID_STANDALONE_CAM)}),
({(DOMAIN, DEV_ID_STANDALONE_CAM), (DOMAIN, DEV_ID_NVR)}),
],
)
async def test_get_device_uid_and_ch(
hass: HomeAssistant,
config_entry: MockConfigEntry,
reolink_connect: MagicMock,
device_registry: dr.DeviceRegistry,
identifiers: set[tuple[str, str]],
) -> None:
"""Test get_device_uid_and_ch with multiple identifiers."""
reolink_connect.channels = [0]
dev_entry = device_registry.async_get_or_create(
identifiers=identifiers,
config_entry_id=config_entry.entry_id,
disabled_by=None,
)
# setup CH 0 and host entities/device
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = get_device_uid_and_ch(dev_entry, config_entry.runtime_data.host)
# always get the uid and channel form the DEV_ID_NVR since is_nvr = True
assert result == ([TEST_UID, TEST_UID_CAM], 0, False)

View File

@ -21,6 +21,7 @@ from homeassistant.components.shelly.const import (
GEN1_RELEASE_URL, GEN1_RELEASE_URL,
GEN2_BETA_RELEASE_URL, GEN2_BETA_RELEASE_URL,
GEN2_RELEASE_URL, GEN2_RELEASE_URL,
UPTIME_DEVIATION,
) )
from homeassistant.components.shelly.utils import ( from homeassistant.components.shelly.utils import (
get_block_channel_name, get_block_channel_name,
@ -188,8 +189,9 @@ async def test_get_device_uptime() -> None:
) == dt_util.as_utc(dt_util.parse_datetime("2019-01-10 18:42:00+00:00")) ) == dt_util.as_utc(dt_util.parse_datetime("2019-01-10 18:42:00+00:00"))
assert get_device_uptime( assert get_device_uptime(
50, dt_util.as_utc(dt_util.parse_datetime("2019-01-10 18:42:00+00:00")) 55 - UPTIME_DEVIATION,
) == dt_util.as_utc(dt_util.parse_datetime("2019-01-10 18:42:10+00:00")) dt_util.as_utc(dt_util.parse_datetime("2019-01-10 18:42:00+00:00")),
) == dt_util.as_utc(dt_util.parse_datetime("2019-01-10 18:43:05+00:00"))
async def test_get_block_input_triggers( async def test_get_block_input_triggers(

View File

@ -107,7 +107,9 @@ def mock_smartthings() -> Generator[AsyncMock]:
"centralite", "centralite",
"da_ref_normal_000001", "da_ref_normal_000001",
"da_ref_normal_01011", "da_ref_normal_01011",
"da_ref_normal_01001",
"vd_network_audio_002s", "vd_network_audio_002s",
"vd_network_audio_003s",
"vd_sensor_light_2023", "vd_sensor_light_2023",
"iphone", "iphone",
"da_sac_ehs_000001_sub", "da_sac_ehs_000001_sub",

View File

@ -0,0 +1,929 @@
{
"components": {
"pantry-01": {
"samsungce.foodDefrost": {
"supportedOptions": {
"value": null
},
"foodType": {
"value": null
},
"weight": {
"value": null
},
"operationTime": {
"value": null
},
"remainingTime": {
"value": null
}
},
"samsungce.fridgePantryInfo": {
"name": {
"value": null
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": ["samsungce.meatAging", "samsungce.foodDefrost"],
"timestamp": "2022-02-07T10:54:05.580Z"
}
},
"samsungce.meatAging": {
"zoneInfo": {
"value": null
},
"supportedMeatTypes": {
"value": null
},
"supportedAgingMethods": {
"value": null
},
"status": {
"value": null
}
},
"samsungce.fridgePantryMode": {
"mode": {
"value": null
},
"supportedModes": {
"value": null
}
}
},
"icemaker": {
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [],
"timestamp": "2022-02-07T10:54:05.580Z"
}
},
"switch": {
"switch": {
"value": "on",
"timestamp": "2025-02-07T12:01:52.528Z"
}
}
},
"scale-10": {
"samsungce.connectionState": {
"connectionState": {
"value": null
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [],
"timestamp": "2022-02-07T10:54:05.580Z"
}
},
"samsungce.weightMeasurement": {
"weight": {
"value": null
}
},
"samsungce.scaleSettings": {
"enabled": {
"value": null
}
},
"samsungce.weightMeasurementCalibration": {}
},
"scale-11": {
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [],
"timestamp": "2022-02-07T10:54:05.580Z"
}
},
"samsungce.weightMeasurement": {
"weight": {
"value": null
}
}
},
"camera-01": {
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": ["switch"],
"timestamp": "2023-12-17T11:19:18.845Z"
}
},
"switch": {
"switch": {
"value": null
}
}
},
"cooler": {
"contactSensor": {
"contact": {
"value": "closed",
"timestamp": "2025-02-09T00:23:41.655Z"
}
},
"samsungce.unavailableCapabilities": {
"unavailableCommands": {
"value": [],
"timestamp": "2024-11-06T12:35:50.411Z"
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [],
"timestamp": "2024-06-17T06:16:33.918Z"
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 37,
"unit": "F",
"timestamp": "2025-02-01T19:39:00.493Z"
}
},
"custom.thermostatSetpointControl": {
"minimumSetpoint": {
"value": 34,
"unit": "F",
"timestamp": "2025-02-01T19:39:00.493Z"
},
"maximumSetpoint": {
"value": 44,
"unit": "F",
"timestamp": "2025-02-01T19:39:00.493Z"
}
},
"thermostatCoolingSetpoint": {
"coolingSetpointRange": {
"value": {
"minimum": 34,
"maximum": 44,
"step": 1
},
"unit": "F",
"timestamp": "2025-02-01T19:39:00.493Z"
},
"coolingSetpoint": {
"value": 37,
"unit": "F",
"timestamp": "2025-02-01T19:39:00.493Z"
}
}
},
"freezer": {
"contactSensor": {
"contact": {
"value": "closed",
"timestamp": "2025-02-09T00:00:44.267Z"
}
},
"samsungce.unavailableCapabilities": {
"unavailableCommands": {
"value": [],
"timestamp": "2024-11-06T12:35:50.411Z"
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": ["samsungce.freezerConvertMode"],
"timestamp": "2024-11-06T09:00:29.743Z"
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 0,
"unit": "F",
"timestamp": "2025-02-01T19:39:00.493Z"
}
},
"custom.thermostatSetpointControl": {
"minimumSetpoint": {
"value": -8,
"unit": "F",
"timestamp": "2025-02-01T19:39:00.493Z"
},
"maximumSetpoint": {
"value": 5,
"unit": "F",
"timestamp": "2025-02-01T19:39:00.493Z"
}
},
"samsungce.freezerConvertMode": {
"supportedFreezerConvertModes": {
"value": [],
"timestamp": "2025-02-01T19:39:00.448Z"
},
"freezerConvertMode": {
"value": null
}
},
"thermostatCoolingSetpoint": {
"coolingSetpointRange": {
"value": {
"minimum": -8,
"maximum": 5,
"step": 1
},
"unit": "F",
"timestamp": "2025-02-01T19:39:00.493Z"
},
"coolingSetpoint": {
"value": 0,
"unit": "F",
"timestamp": "2025-02-01T19:39:00.493Z"
}
}
},
"main": {
"contactSensor": {
"contact": {
"value": "closed",
"timestamp": "2025-02-09T00:23:41.655Z"
}
},
"samsungce.viewInside": {
"supportedFocusAreas": {
"value": ["mainShelves"],
"timestamp": "2025-02-01T19:39:00.946Z"
},
"contents": {
"value": [
{
"fileId": "d3e1f875-f8b3-a031-737b-366eaa227773",
"mimeType": "image/jpeg",
"expiredTime": "2025-01-20T16:17:04Z",
"focusArea": "mainShelves"
},
{
"fileId": "9fccb6b4-e71f-6c7f-9935-f6082bb6ccfe",
"mimeType": "image/jpeg",
"expiredTime": "2025-01-20T16:17:04Z",
"focusArea": "mainShelves"
},
{
"fileId": "20b57a4d-b7fc-17fc-3a03-0fb84fb4efab",
"mimeType": "image/jpeg",
"expiredTime": "2025-01-20T16:17:05Z",
"focusArea": "mainShelves"
}
],
"timestamp": "2025-01-20T16:07:05.423Z"
},
"lastUpdatedTime": {
"value": "2025-02-07T12:01:52Z",
"timestamp": "2025-02-07T12:01:52.585Z"
}
},
"samsungce.fridgeFoodList": {
"outOfSyncChanges": {
"value": null
},
"refreshResult": {
"value": null
}
},
"samsungce.deviceIdentification": {
"micomAssayCode": {
"value": null
},
"modelName": {
"value": null
},
"serialNumber": {
"value": null
},
"serialNumberExtra": {
"value": null
},
"modelClassificationCode": {
"value": null
},
"description": {
"value": null
},
"releaseYear": {
"value": 19,
"timestamp": "2024-11-06T09:00:29.743Z"
},
"binaryId": {
"value": "24K_REF_LCD_FHUB9.0",
"timestamp": "2025-02-07T12:01:53.067Z"
}
},
"samsungce.quickControl": {
"version": {
"value": "1.0",
"timestamp": "2025-02-01T19:39:01.848Z"
}
},
"custom.fridgeMode": {
"fridgeModeValue": {
"value": null
},
"fridgeMode": {
"value": null
},
"supportedFridgeModes": {
"value": null
}
},
"ocf": {
"st": {
"value": "2024-11-08T11:56:59Z",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"mndt": {
"value": "",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"mnfv": {
"value": "20240616.213423",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"mnhw": {
"value": "",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"di": {
"value": "7d3feb98-8a36-4351-c362-5e21ad3a78dd",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"mnsl": {
"value": "",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"dmv": {
"value": "res.1.1.0,sh.1.1.0",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"n": {
"value": "Family Hub",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"mnmo": {
"value": "24K_REF_LCD_FHUB9.0|00113141|0002034e051324200103000000000000",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"vid": {
"value": "DA-REF-NORMAL-01001",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"mnmn": {
"value": "Samsung Electronics",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"mnml": {
"value": "",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"mnpv": {
"value": "7.0",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"mnos": {
"value": "Tizen",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"pi": {
"value": "7d3feb98-8a36-4351-c362-5e21ad3a78dd",
"timestamp": "2025-01-02T12:37:43.756Z"
},
"icv": {
"value": "core.1.1.0",
"timestamp": "2025-01-02T12:37:43.756Z"
}
},
"samsungce.fridgeVacationMode": {
"vacationMode": {
"value": null
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [
"thermostatCoolingSetpoint",
"temperatureMeasurement",
"custom.fridgeMode",
"custom.deviceReportStateConfiguration",
"samsungce.fridgeFoodList",
"samsungce.runestoneHomeContext",
"demandResponseLoadControl",
"samsungce.fridgeVacationMode",
"samsungce.sabbathMode"
],
"timestamp": "2025-02-08T23:57:45.739Z"
}
},
"samsungce.driverVersion": {
"versionNumber": {
"value": 24090102,
"timestamp": "2024-11-06T09:00:29.743Z"
}
},
"sec.diagnosticsInformation": {
"logType": {
"value": ["errCode", "dump"],
"timestamp": "2025-02-01T19:39:00.523Z"
},
"endpoint": {
"value": "SSM",
"timestamp": "2025-02-01T19:39:00.523Z"
},
"minVersion": {
"value": "1.0",
"timestamp": "2025-02-01T19:39:00.523Z"
},
"signinPermission": {
"value": null
},
"setupId": {
"value": "500",
"timestamp": "2025-02-01T19:39:00.523Z"
},
"protocolType": {
"value": "wifi_https",
"timestamp": "2025-02-01T19:39:00.523Z"
},
"tsId": {
"value": null
},
"mnId": {
"value": "0AJT",
"timestamp": "2025-02-01T19:39:00.523Z"
},
"dumpType": {
"value": "file",
"timestamp": "2025-02-01T19:39:00.523Z"
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": null
}
},
"custom.deviceReportStateConfiguration": {
"reportStateRealtimePeriod": {
"value": null
},
"reportStateRealtime": {
"value": {
"state": "disabled"
},
"timestamp": "2025-02-01T19:39:00.345Z"
},
"reportStatePeriod": {
"value": "enabled",
"timestamp": "2025-02-01T19:39:00.345Z"
}
},
"thermostatCoolingSetpoint": {
"coolingSetpointRange": {
"value": null
},
"coolingSetpoint": {
"value": null
}
},
"custom.disabledComponents": {
"disabledComponents": {
"value": [
"icemaker-02",
"icemaker-03",
"pantry-01",
"camera-01",
"scale-10",
"scale-11"
],
"timestamp": "2025-02-07T12:01:52.638Z"
}
},
"demandResponseLoadControl": {
"drlcStatus": {
"value": {
"drlcType": 1,
"drlcLevel": 0,
"duration": 0,
"override": false
},
"timestamp": "2025-02-01T19:38:59.899Z"
}
},
"samsungce.sabbathMode": {
"supportedActions": {
"value": null
},
"status": {
"value": null
}
},
"powerConsumptionReport": {
"powerConsumption": {
"value": {
"energy": 4381422,
"deltaEnergy": 27,
"power": 144,
"powerEnergy": 27.01890500307083,
"persistedEnergy": 0,
"energySaved": 0,
"start": "2025-02-09T00:13:39Z",
"end": "2025-02-09T00:25:23Z"
},
"timestamp": "2025-02-09T00:25:23.843Z"
}
},
"refresh": {},
"samsungce.runestoneHomeContext": {
"supportedContexts": {
"value": [
{
"context": "HOME_IN",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "ASLEEP",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "AWAKE",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "COOKING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "FINISH_COOKING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "EATING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "FINISH_EATING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "DOING_LAUNDRY",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "FINISH_DOING_LAUNDRY",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "CLEANING_HOUSE",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "FINISH_CLEANING_HOUSE",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "MUSIC_LISTENING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "FINISH_MUSIC_LISTENING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "AIR_CONDITIONING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "FINISH_AIR_CONDITIONING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "WASHING_DISHES",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "FINISH_WASHING_DISHES",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "CARING_CLOTHING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "FINISH_CARING_CLOTHING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "WATCHING_TV",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "FINISH_WATCHING_TV",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "BEFORE_BEDTIME",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "BEFORE_COOKING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "BEFORE_HOME_OUT",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "ORDERING_DELIVERY_FOOD",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "FINISH_ORDERING_DELIVERY_FOOD",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "ONLINE_GROCERY_SHOPPING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
},
{
"context": "FINISH_ONLINE_GROCERY_SHOPPING",
"place": "HOME",
"startTime": "99:99",
"endTime": "99:99"
}
],
"timestamp": "2025-02-01T19:39:02.150Z"
}
},
"execute": {
"data": {
"value": {
"payload": {
"rt": ["x.com.samsung.da.fridge"],
"if": ["oic.if.a"],
"x.com.samsung.da.rapidFridge": "Off",
"x.com.samsung.da.rapidFreezing": "Off"
}
},
"data": {
"href": "/refrigeration/vs/0"
},
"timestamp": "2024-03-26T09:06:17.169Z"
}
},
"sec.wifiConfiguration": {
"autoReconnection": {
"value": true,
"timestamp": "2025-02-01T19:39:01.951Z"
},
"minVersion": {
"value": "1.0",
"timestamp": "2025-02-01T19:39:01.951Z"
},
"supportedWiFiFreq": {
"value": ["2.4G", "5G"],
"timestamp": "2025-02-01T19:39:01.951Z"
},
"supportedAuthType": {
"value": ["OPEN", "WEP", "WPA-PSK", "WPA2-PSK"],
"timestamp": "2025-02-01T19:39:01.951Z"
},
"protocolType": {
"value": ["helper_hotspot"],
"timestamp": "2025-02-01T19:39:01.951Z"
}
},
"refrigeration": {
"defrost": {
"value": "off",
"timestamp": "2025-02-01T19:38:59.276Z"
},
"rapidCooling": {
"value": "off",
"timestamp": "2025-02-01T19:39:00.497Z"
},
"rapidFreezing": {
"value": "off",
"timestamp": "2025-02-01T19:39:00.497Z"
}
},
"samsungce.powerCool": {
"activated": {
"value": false,
"timestamp": "2025-02-01T19:39:00.497Z"
}
},
"custom.energyType": {
"energyType": {
"value": "2.0",
"timestamp": "2022-02-07T10:54:05.580Z"
},
"energySavingSupport": {
"value": false,
"timestamp": "2022-02-07T10:57:35.490Z"
},
"drMaxDuration": {
"value": 1440,
"unit": "min",
"timestamp": "2022-02-07T11:50:40.228Z"
},
"energySavingLevel": {
"value": null
},
"energySavingInfo": {
"value": null
},
"supportedEnergySavingLevels": {
"value": null
},
"energySavingOperation": {
"value": null
},
"notificationTemplateID": {
"value": null
},
"energySavingOperationSupport": {
"value": false,
"timestamp": "2022-02-07T11:50:40.228Z"
}
},
"samsungce.softwareUpdate": {
"targetModule": {
"value": {},
"timestamp": "2025-02-01T19:39:00.200Z"
},
"otnDUID": {
"value": "2DCEZFTFQZPMO",
"timestamp": "2025-02-01T19:39:00.523Z"
},
"lastUpdatedDate": {
"value": null
},
"availableModules": {
"value": [],
"timestamp": "2025-02-01T19:39:00.523Z"
},
"newVersionAvailable": {
"value": false,
"timestamp": "2025-02-01T19:39:00.200Z"
},
"operatingState": {
"value": null
},
"progress": {
"value": null
}
},
"samsungce.powerFreeze": {
"activated": {
"value": false,
"timestamp": "2025-02-01T19:39:00.497Z"
}
},
"custom.waterFilter": {
"waterFilterUsageStep": {
"value": 1,
"timestamp": "2025-02-01T19:38:59.973Z"
},
"waterFilterResetType": {
"value": ["replaceable"],
"timestamp": "2025-02-01T19:38:59.973Z"
},
"waterFilterCapacity": {
"value": null
},
"waterFilterLastResetDate": {
"value": null
},
"waterFilterUsage": {
"value": 52,
"timestamp": "2025-02-08T05:06:45.769Z"
},
"waterFilterStatus": {
"value": "normal",
"timestamp": "2025-02-01T19:38:59.973Z"
}
}
},
"cvroom": {
"custom.fridgeMode": {
"fridgeModeValue": {
"value": null
},
"fridgeMode": {
"value": "CV_FDR_DELI",
"timestamp": "2025-02-01T19:39:00.448Z"
},
"supportedFridgeModes": {
"value": [
"CV_FDR_WINE",
"CV_FDR_DELI",
"CV_FDR_BEVERAGE",
"CV_FDR_MEAT"
],
"timestamp": "2025-02-01T19:39:00.448Z"
}
},
"contactSensor": {
"contact": {
"value": "closed",
"timestamp": "2025-02-08T23:22:04.631Z"
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [],
"timestamp": "2021-07-27T01:19:43.145Z"
}
}
},
"icemaker-02": {
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [],
"timestamp": "2022-07-28T18:47:07.039Z"
}
},
"switch": {
"switch": {
"value": null
}
}
},
"icemaker-03": {
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [],
"timestamp": "2023-12-15T01:05:09.803Z"
}
},
"switch": {
"switch": {
"value": null
}
}
}
}
}

View File

@ -0,0 +1,231 @@
{
"components": {
"main": {
"samsungvd.soundFrom": {
"mode": {
"value": 29,
"timestamp": "2025-04-05T13:51:47.865Z"
},
"detailName": {
"value": "None",
"timestamp": "2025-04-05T13:51:50.230Z"
}
},
"audioVolume": {
"volume": {
"value": 6,
"unit": "%",
"timestamp": "2025-04-17T11:17:25.272Z"
}
},
"samsungvd.audioGroupInfo": {
"role": {
"value": null
},
"channel": {
"value": null
},
"status": {
"value": null
}
},
"refresh": {},
"audioNotification": {},
"execute": {
"data": {
"value": null
}
},
"samsungvd.audioInputSource": {
"supportedInputSources": {
"value": ["D.IN", "BT", "WIFI"],
"timestamp": "2025-03-18T19:11:54.071Z"
},
"inputSource": {
"value": "D.IN",
"timestamp": "2025-04-17T11:18:02.048Z"
}
},
"switch": {
"switch": {
"value": "off",
"timestamp": "2025-04-17T14:42:04.704Z"
}
},
"sec.wifiConfiguration": {
"autoReconnection": {
"value": true,
"timestamp": "2025-03-18T19:11:54.484Z"
},
"minVersion": {
"value": "1.0",
"timestamp": "2025-03-18T19:11:54.484Z"
},
"supportedWiFiFreq": {
"value": ["2.4G", "5G"],
"timestamp": "2025-03-18T19:11:54.484Z"
},
"supportedAuthType": {
"value": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"EAP",
"SAE",
"OWE",
"FT-PSK"
],
"timestamp": "2025-03-18T19:11:54.484Z"
},
"protocolType": {
"value": ["ble_ocf"],
"timestamp": "2025-03-18T19:11:54.484Z"
}
},
"ocf": {
"st": {
"value": "1970-01-01T00:00:47Z",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"mndt": {
"value": "2024-01-01",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"mnfv": {
"value": "SAT-MT8532D24WWC-1016.0",
"timestamp": "2025-02-21T16:47:38.134Z"
},
"mnhw": {
"value": "",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"di": {
"value": "a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"mnsl": {
"value": "",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"dmv": {
"value": "res.1.1.0,sh.1.1.0",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"n": {
"value": "Soundbar",
"timestamp": "2025-02-21T16:47:38.134Z"
},
"mnmo": {
"value": "HW-S60D",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"vid": {
"value": "VD-NetworkAudio-003S",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"mnmn": {
"value": "Samsung Electronics",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"mnml": {
"value": "",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"mnpv": {
"value": "8.0",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"mnos": {
"value": "Tizen",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"pi": {
"value": "a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6",
"timestamp": "2025-02-21T15:09:52.348Z"
},
"icv": {
"value": "core.1.1.0",
"timestamp": "2025-02-21T15:09:52.348Z"
}
},
"samsungvd.supportsFeatures": {
"mediaOutputSupported": {
"value": null
},
"imeAdvSupported": {
"value": null
},
"wifiUpdateSupport": {
"value": true,
"timestamp": "2025-03-18T19:11:53.853Z"
},
"executableServiceList": {
"value": null
},
"remotelessSupported": {
"value": null
},
"artSupported": {
"value": null
},
"mobileCamSupported": {
"value": null
}
},
"sec.diagnosticsInformation": {
"logType": {
"value": ["errCode", "dump"],
"timestamp": "2025-03-18T19:11:54.336Z"
},
"endpoint": {
"value": "PIPER",
"timestamp": "2025-03-18T19:11:54.336Z"
},
"minVersion": {
"value": "3.0",
"timestamp": "2025-03-18T19:11:54.336Z"
},
"signinPermission": {
"value": null
},
"setupId": {
"value": "301",
"timestamp": "2025-03-18T19:11:54.336Z"
},
"protocolType": {
"value": "ble_ocf",
"timestamp": "2025-03-18T19:11:54.336Z"
},
"tsId": {
"value": "VD02",
"timestamp": "2025-03-18T19:11:54.336Z"
},
"mnId": {
"value": "0AJK",
"timestamp": "2025-03-18T19:11:54.336Z"
},
"dumpType": {
"value": "file",
"timestamp": "2025-03-18T19:11:54.336Z"
}
},
"audioMute": {
"mute": {
"value": "muted",
"timestamp": "2025-04-17T11:36:04.814Z"
}
},
"samsungvd.thingStatus": {
"updatedTime": {
"value": 1744900925,
"timestamp": "2025-04-17T14:42:04.770Z"
},
"status": {
"value": "Idle",
"timestamp": "2025-03-18T19:11:54.101Z"
}
}
}
}
}

View File

@ -0,0 +1,433 @@
{
"items": [
{
"deviceId": "7d3feb98-8a36-4351-c362-5e21ad3a78dd",
"name": "Family Hub",
"label": "Refrigerator",
"manufacturerName": "Samsung Electronics",
"presentationId": "DA-REF-NORMAL-01001",
"deviceManufacturerCode": "Samsung Electronics",
"locationId": "2487472a-06c4-4bce-8f4c-700c5f8644f8",
"ownerId": "b603d7e8-6066-4e10-8102-afa752a63816",
"roomId": "acaa060a-7c19-4579-8a4a-5ad891a2f0c1",
"deviceTypeName": "Samsung OCF Refrigerator",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "contactSensor",
"version": 1
},
{
"id": "execute",
"version": 1
},
{
"id": "ocf",
"version": 1
},
{
"id": "powerConsumptionReport",
"version": 1
},
{
"id": "demandResponseLoadControl",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "refrigeration",
"version": 1
},
{
"id": "temperatureMeasurement",
"version": 1
},
{
"id": "thermostatCoolingSetpoint",
"version": 1
},
{
"id": "custom.deviceReportStateConfiguration",
"version": 1
},
{
"id": "custom.energyType",
"version": 1
},
{
"id": "custom.fridgeMode",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
},
{
"id": "custom.disabledComponents",
"version": 1
},
{
"id": "custom.waterFilter",
"version": 1
},
{
"id": "samsungce.fridgeFoodList",
"version": 1
},
{
"id": "samsungce.softwareUpdate",
"version": 1
},
{
"id": "samsungce.deviceIdentification",
"version": 1
},
{
"id": "samsungce.driverVersion",
"version": 1
},
{
"id": "samsungce.fridgeVacationMode",
"version": 1
},
{
"id": "samsungce.powerCool",
"version": 1
},
{
"id": "samsungce.powerFreeze",
"version": 1
},
{
"id": "samsungce.sabbathMode",
"version": 1
},
{
"id": "samsungce.viewInside",
"version": 1
},
{
"id": "samsungce.runestoneHomeContext",
"version": 1
},
{
"id": "samsungce.quickControl",
"version": 1
},
{
"id": "sec.diagnosticsInformation",
"version": 1
},
{
"id": "sec.wifiConfiguration",
"version": 1
}
],
"categories": [
{
"name": "Refrigerator",
"categoryType": "manufacturer"
}
]
},
{
"id": "freezer",
"label": "freezer",
"capabilities": [
{
"id": "contactSensor",
"version": 1
},
{
"id": "temperatureMeasurement",
"version": 1
},
{
"id": "thermostatCoolingSetpoint",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
},
{
"id": "custom.thermostatSetpointControl",
"version": 1
},
{
"id": "samsungce.freezerConvertMode",
"version": 1
},
{
"id": "samsungce.unavailableCapabilities",
"version": 1
}
],
"categories": [
{
"name": "Other",
"categoryType": "manufacturer"
}
]
},
{
"id": "cooler",
"label": "cooler",
"capabilities": [
{
"id": "contactSensor",
"version": 1
},
{
"id": "temperatureMeasurement",
"version": 1
},
{
"id": "thermostatCoolingSetpoint",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
},
{
"id": "custom.thermostatSetpointControl",
"version": 1
},
{
"id": "samsungce.unavailableCapabilities",
"version": 1
}
],
"categories": [
{
"name": "Other",
"categoryType": "manufacturer"
}
]
},
{
"id": "cvroom",
"label": "cvroom",
"capabilities": [
{
"id": "contactSensor",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
},
{
"id": "custom.fridgeMode",
"version": 1
}
],
"categories": [
{
"name": "Other",
"categoryType": "manufacturer"
}
]
},
{
"id": "icemaker",
"label": "icemaker",
"capabilities": [
{
"id": "switch",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
}
],
"categories": [
{
"name": "Other",
"categoryType": "manufacturer"
}
]
},
{
"id": "icemaker-02",
"label": "icemaker-02",
"capabilities": [
{
"id": "switch",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
}
],
"categories": [
{
"name": "Other",
"categoryType": "manufacturer"
}
]
},
{
"id": "icemaker-03",
"label": "icemaker-03",
"capabilities": [
{
"id": "switch",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
}
],
"categories": [
{
"name": "Other",
"categoryType": "manufacturer"
}
]
},
{
"id": "scale-10",
"label": "scale-10",
"capabilities": [
{
"id": "samsungce.weightMeasurement",
"version": 1
},
{
"id": "samsungce.weightMeasurementCalibration",
"version": 1
},
{
"id": "samsungce.connectionState",
"version": 1
},
{
"id": "samsungce.scaleSettings",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
}
],
"categories": [
{
"name": "Other",
"categoryType": "manufacturer"
}
]
},
{
"id": "scale-11",
"label": "scale-11",
"capabilities": [
{
"id": "samsungce.weightMeasurement",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
}
],
"categories": [
{
"name": "Other",
"categoryType": "manufacturer"
}
]
},
{
"id": "pantry-01",
"label": "pantry-01",
"capabilities": [
{
"id": "samsungce.fridgePantryInfo",
"version": 1
},
{
"id": "samsungce.fridgePantryMode",
"version": 1
},
{
"id": "samsungce.meatAging",
"version": 1
},
{
"id": "samsungce.foodDefrost",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
}
],
"categories": [
{
"name": "Other",
"categoryType": "manufacturer"
}
]
},
{
"id": "camera-01",
"label": "camera-01",
"capabilities": [
{
"id": "switch",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
}
],
"categories": [
{
"name": "Other",
"categoryType": "manufacturer"
}
]
}
],
"createTime": "2021-07-27T01:19:42.051Z",
"profile": {
"id": "4c654f1b-8ef4-35b0-920e-c12568554213"
},
"ocf": {
"ocfDeviceType": "oic.d.refrigerator",
"name": "Family Hub",
"specVersion": "core.1.1.0",
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
"manufacturerName": "Samsung Electronics",
"modelNumber": "24K_REF_LCD_FHUB9.0|00113141|0002034e051324200103000000000000",
"platformVersion": "7.0",
"platformOS": "Tizen",
"hwVersion": "",
"firmwareVersion": "20240616.213423",
"vendorId": "DA-REF-NORMAL-01001",
"vendorResourceClientServerVersion": "4.0.22",
"locale": "",
"lastSignupTime": "2021-07-27T01:19:40.244392Z",
"transferCandidate": false,
"additionalAuthCodeRequired": false
},
"type": "OCF",
"restrictionTier": 0,
"allowed": [],
"executionContext": "CLOUD"
}
],
"_links": {}
}

View File

@ -0,0 +1,115 @@
{
"items": [
{
"deviceId": "a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6",
"name": "Soundbar",
"label": "Soundbar",
"manufacturerName": "Samsung Electronics",
"presentationId": "VD-NetworkAudio-003S",
"deviceManufacturerCode": "Samsung Electronics",
"locationId": "6bdf6730-8167-488b-8645-d0c5046ff763",
"ownerId": "15f0ae72-da51-14e2-65cf-ef59ae867e7f",
"roomId": "3b0fe9a8-51d6-49cf-b64a-8a719013c0a7",
"deviceTypeName": "Samsung OCF Network Audio Player",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "ocf",
"version": 1
},
{
"id": "execute",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "switch",
"version": 1
},
{
"id": "audioVolume",
"version": 1
},
{
"id": "audioMute",
"version": 1
},
{
"id": "samsungvd.audioInputSource",
"version": 1
},
{
"id": "audioNotification",
"version": 1
},
{
"id": "samsungvd.soundFrom",
"version": 1
},
{
"id": "sec.diagnosticsInformation",
"version": 1
},
{
"id": "samsungvd.thingStatus",
"version": 1
},
{
"id": "samsungvd.supportsFeatures",
"version": 1
},
{
"id": "sec.wifiConfiguration",
"version": 1
},
{
"id": "samsungvd.audioGroupInfo",
"version": 1,
"ephemeral": true
}
],
"categories": [
{
"name": "NetworkAudio",
"categoryType": "manufacturer"
}
],
"optional": false
}
],
"createTime": "2025-02-21T14:25:21.843Z",
"profile": {
"id": "25504ad5-8563-3b07-8770-e52ad29a9c5a"
},
"ocf": {
"ocfDeviceType": "oic.d.networkaudio",
"name": "Soundbar",
"specVersion": "core.1.1.0",
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
"manufacturerName": "Samsung Electronics",
"modelNumber": "HW-S60D",
"platformVersion": "8.0",
"platformOS": "Tizen",
"hwVersion": "",
"firmwareVersion": "SAT-MT8532D24WWC-1016.0",
"vendorId": "VD-NetworkAudio-003S",
"vendorResourceClientServerVersion": "4.0.26",
"lastSignupTime": "2025-03-18T19:11:51.176292902Z",
"transferCandidate": false,
"additionalAuthCodeRequired": false
},
"type": "OCF",
"restrictionTier": 0,
"allowed": null,
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}

View File

@ -761,6 +761,150 @@
'state': 'off', 'state': 'off',
}) })
# --- # ---
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_cooler_door-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': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.refrigerator_cooler_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Cooler door',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'cooler_door',
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_contactSensor_contact_contact',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_cooler_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'Refrigerator Cooler door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.refrigerator_cooler_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_coolselect_door-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': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.refrigerator_coolselect_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'CoolSelect+ door',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'cool_select_plus_door',
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cvroom_contactSensor_contact_contact',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_coolselect_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'Refrigerator CoolSelect+ door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.refrigerator_coolselect_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_freezer_door-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': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.refrigerator_freezer_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Freezer door',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'freezer_door',
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_freezer_contactSensor_contact_contact',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_freezer_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'Refrigerator Freezer door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.refrigerator_freezer_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_cooler_door-entry] # name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_cooler_door-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -187,3 +187,50 @@
'state': 'unknown', 'state': 'unknown',
}) })
# --- # ---
# name: test_all_entities[da_ref_normal_01001][button.refrigerator_reset_water_filter-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.refrigerator_reset_water_filter',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset water filter',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'reset_water_filter',
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_custom.waterFilter_resetWaterFilter',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ref_normal_01001][button.refrigerator_reset_water_filter-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Refrigerator Reset water filter',
}),
'context': <ANY>,
'entity_id': 'button.refrigerator_reset_water_filter',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@ -629,6 +629,39 @@
'via_device_id': None, 'via_device_id': None,
}) })
# --- # ---
# name: test_devices[da_ref_normal_01001]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': '',
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'7d3feb98-8a36-4351-c362-5e21ad3a78dd',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Samsung Electronics',
'model': '24K_REF_LCD_FHUB9.0',
'model_id': None,
'name': 'Refrigerator',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '20240616.213423',
'via_device_id': None,
})
# ---
# name: test_devices[da_ref_normal_01011] # name: test_devices[da_ref_normal_01011]
DeviceRegistryEntrySnapshot({ DeviceRegistryEntrySnapshot({
'area_id': None, 'area_id': None,
@ -1652,6 +1685,39 @@
'via_device_id': None, 'via_device_id': None,
}) })
# --- # ---
# name: test_devices[vd_network_audio_003s]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': '',
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Samsung Electronics',
'model': 'HW-S60D',
'model_id': None,
'name': 'Soundbar',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': 'SAT-MT8532D24WWC-1016.0',
'via_device_id': None,
})
# ---
# name: test_devices[vd_sensor_light_2023] # name: test_devices[vd_sensor_light_2023]
DeviceRegistryEntrySnapshot({ DeviceRegistryEntrySnapshot({
'area_id': None, 'area_id': None,

View File

@ -231,6 +231,56 @@
'state': 'on', 'state': 'on',
}) })
# --- # ---
# name: test_all_entities[vd_network_audio_003s][media_player.soundbar-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.soundbar',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <MediaPlayerDeviceClass.SPEAKER: 'speaker'>,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': <MediaPlayerEntityFeature: 1420>,
'translation_key': None,
'unique_id': 'a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6_main',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[vd_network_audio_003s][media_player.soundbar-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'speaker',
'friendly_name': 'Soundbar',
'supported_features': <MediaPlayerEntityFeature: 1420>,
}),
'context': <ANY>,
'entity_id': 'media_player.soundbar',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[vd_stv_2017_k][media_player.tv_samsung_8_series_49-entry] # name: test_all_entities[vd_stv_2017_k][media_player.tv_samsung_8_series_49-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -4049,6 +4049,283 @@
'state': '0.0135559777781698', 'state': '0.0135559777781698',
}) })
# --- # ---
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.refrigerator_energy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_energy_meter',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Refrigerator Energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.refrigerator_energy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '4381.422',
})
# ---
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy_difference-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.refrigerator_energy_difference',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy difference',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'energy_difference',
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy_difference-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Refrigerator Energy difference',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.refrigerator_energy_difference',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.027',
})
# ---
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy_saved-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.refrigerator_energy_saved',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy saved',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'energy_saved',
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_energySaved_meter',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy_saved-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Refrigerator Energy saved',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.refrigerator_energy_saved',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.refrigerator_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Power',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_power_meter',
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Refrigerator Power',
'power_consumption_end': '2025-02-09T00:25:23Z',
'power_consumption_start': '2025-02-09T00:13:39Z',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.refrigerator_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '144',
})
# ---
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.refrigerator_power_energy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Power energy',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'power_energy',
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_powerEnergy_meter',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Refrigerator Power energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.refrigerator_power_energy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0270189050030708',
})
# ---
# name: test_all_entities[da_ref_normal_01011][sensor.frigo_energy-entry] # name: test_all_entities[da_ref_normal_01011][sensor.frigo_energy-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -93,6 +93,53 @@
'state': 'off', 'state': 'off',
}) })
# --- # ---
# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_ice_maker-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': 'switch',
'entity_category': None,
'entity_id': 'switch.refrigerator_ice_maker',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Ice maker',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'ice_maker',
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_icemaker_switch_switch_switch',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_ice_maker-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Refrigerator Ice maker',
}),
'context': <ANY>,
'entity_id': 'switch.refrigerator_ice_maker',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[da_rvc_normal_000001][switch.robot_vacuum-entry] # name: test_all_entities[da_rvc_normal_000001][switch.robot_vacuum-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -181,13 +181,13 @@ async def test_assist_api(
assert len(llm.async_get_apis(hass)) == 1 assert len(llm.async_get_apis(hass)) == 1
api = await llm.async_get_api(hass, "assist", llm_context) api = await llm.async_get_api(hass, "assist", llm_context)
assert [tool.name for tool in api.tools] == ["get_home_state"] assert [tool.name for tool in api.tools] == ["GetLiveContext"]
# Match all # Match all
intent_handler.platforms = None intent_handler.platforms = None
api = await llm.async_get_api(hass, "assist", llm_context) api = await llm.async_get_api(hass, "assist", llm_context)
assert [tool.name for tool in api.tools] == ["test_intent", "get_home_state"] assert [tool.name for tool in api.tools] == ["test_intent", "GetLiveContext"]
# Match specific domain # Match specific domain
intent_handler.platforms = {"light"} intent_handler.platforms = {"light"}
@ -575,7 +575,7 @@ async def test_assist_api_prompt(
suggested_area="Test Area 2", suggested_area="Test Area 2",
) )
) )
exposed_entities_prompt = """An overview of the areas and the devices in this smart home: exposed_entities_prompt = """Live Context: An overview of the areas and the devices in this smart home:
- names: Kitchen - names: Kitchen
domain: light domain: light
state: 'on' state: 'on'
@ -623,7 +623,7 @@ async def test_assist_api_prompt(
state: unavailable state: unavailable
areas: Test Area 2 areas: Test Area 2
""" """
stateless_exposed_entities_prompt = """An overview of the areas and the devices in this smart home: stateless_exposed_entities_prompt = """Static Context: An overview of the areas and the devices in this smart home:
- names: Kitchen - names: Kitchen
domain: light domain: light
- names: Living Room - names: Living Room
@ -669,17 +669,30 @@ async def test_assist_api_prompt(
"When a user asks to turn on all devices of a specific type, " "When a user asks to turn on all devices of a specific type, "
"ask user to specify an area, unless there is only one device of that type." "ask user to specify an area, unless there is only one device of that type."
) )
dynamic_context_prompt = """You ARE equipped to answer questions about the current state of
the home using the `GetLiveContext` tool. This is a primary function. Do not state you lack the
functionality if the question requires live data.
If the user asks about device existence/type (e.g., "Do I have lights in the bedroom?"): Answer
from the static context below.
If the user asks about the CURRENT state, value, or mode (e.g., "Is the lock locked?",
"Is the fan on?", "What mode is the thermostat in?", "What is the temperature outside?"):
1. Recognize this requires live data.
2. You MUST call `GetLiveContext`. This tool will provide the needed real-time information (like temperature from the local weather, lock status, etc.).
3. Use the tool's response** to answer the user accurately (e.g., "The temperature outside is [value from tool].").
For general knowledge questions not about the home: Answer truthfully from internal knowledge.
"""
api = await llm.async_get_api(hass, "assist", llm_context) api = await llm.async_get_api(hass, "assist", llm_context)
assert api.api_prompt == ( assert api.api_prompt == (
f"""{first_part_prompt} f"""{first_part_prompt}
{area_prompt} {area_prompt}
{no_timer_prompt} {no_timer_prompt}
{dynamic_context_prompt}
{stateless_exposed_entities_prompt}""" {stateless_exposed_entities_prompt}"""
) )
# Verify that the get_home_state tool returns the same results as the exposed_entities_prompt # Verify that the GetLiveContext tool returns the same results as the exposed_entities_prompt
result = await api.async_call_tool( result = await api.async_call_tool(
llm.ToolInput(tool_name="get_home_state", tool_args={}) llm.ToolInput(tool_name="GetLiveContext", tool_args={})
) )
assert result == { assert result == {
"success": True, "success": True,
@ -697,6 +710,7 @@ async def test_assist_api_prompt(
f"""{first_part_prompt} f"""{first_part_prompt}
{area_prompt} {area_prompt}
{no_timer_prompt} {no_timer_prompt}
{dynamic_context_prompt}
{stateless_exposed_entities_prompt}""" {stateless_exposed_entities_prompt}"""
) )
@ -712,6 +726,7 @@ async def test_assist_api_prompt(
f"""{first_part_prompt} f"""{first_part_prompt}
{area_prompt} {area_prompt}
{no_timer_prompt} {no_timer_prompt}
{dynamic_context_prompt}
{stateless_exposed_entities_prompt}""" {stateless_exposed_entities_prompt}"""
) )
@ -723,6 +738,7 @@ async def test_assist_api_prompt(
assert api.api_prompt == ( assert api.api_prompt == (
f"""{first_part_prompt} f"""{first_part_prompt}
{area_prompt} {area_prompt}
{dynamic_context_prompt}
{stateless_exposed_entities_prompt}""" {stateless_exposed_entities_prompt}"""
) )