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

View File

@ -81,4 +81,7 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity):
@property
def is_on(self) -> bool:
"""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",
"iot_class": "local_polling",
"loggers": ["devolo_plc_api"],
"requirements": ["devolo-plc-api==1.4.1"],
"requirements": ["devolo-plc-api==1.5.1"],
"zeroconf": [
{
"type": "_dvl-deviceapi._tcp.local.",

View File

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

View File

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

View File

@ -7,7 +7,7 @@ from datetime import timedelta
import logging
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.core import HomeAssistant
@ -65,6 +65,6 @@ class ECDataUpdateCoordinator[DataT: ECDataType](DataUpdateCoordinator[DataT]):
"""Fetch data from EC."""
try:
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
return self.ec_data

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"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",
translation_key="timestamp",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.metadata.get("timestamp"),
value_fn=lambda data: data.metadata.timestamp,
),
ECSensorEntityDescription(
key="uv_index",
@ -289,7 +289,7 @@ class ECBaseSensorEntity[DataT: ECDataType](
super().__init__(coordinator)
self.entity_description = description
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_device_info = coordinator.device_info
@ -313,8 +313,8 @@ class ECSensorEntity[DataT: ECDataType](ECBaseSensorEntity[DataT]):
"""Initialize the sensor."""
super().__init__(coordinator, description)
self._attr_extra_state_attributes = {
ATTR_LOCATION: self._ec_data.metadata.get("location"),
ATTR_STATION: self._ec_data.metadata.get("station"),
ATTR_LOCATION: self._ec_data.metadata.location,
ATTR_STATION: self._ec_data.metadata.station,
}
@ -329,8 +329,8 @@ class ECAlertSensorEntity(ECBaseSensorEntity[ECWeather]):
return None
extra_state_attrs = {
ATTR_LOCATION: self._ec_data.metadata.get("location"),
ATTR_STATION: self._ec_data.metadata.get("station"),
ATTR_LOCATION: self._ec_data.metadata.location,
ATTR_STATION: self._ec_data.metadata.station,
}
for index, alert in enumerate(value, start=1):
extra_state_attrs[f"alert_{index}"] = alert.get("title")

View File

@ -115,7 +115,7 @@ class ECWeatherEntity(
"""Initialize Environment Canada weather."""
super().__init__(coordinator)
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_unique_id = _calculate_unique_id(
coordinator.config_entry.unique_id, False

View File

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

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"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
for event in event_message.data.items:
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)
if setting_key in settings:
settings[setting_key].value = event.value

View File

@ -61,11 +61,14 @@ OPTIONS_SCHEMA = vol.Schema(
_LOGGER = logging.getLogger(__name__)
def _get_data_schema(hass: HomeAssistant) -> vol.Schema:
async def _get_data_schema(hass: HomeAssistant) -> vol.Schema:
default_location = {
CONF_LATITUDE: hass.config.latitude,
CONF_LONGITUDE: hass.config.longitude,
}
get_timezones: list[str] = list(
await hass.async_add_executor_job(zoneinfo.available_timezones)
)
return vol.Schema(
{
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_ELEVATION, default=hass.config.elevation): int,
vol.Optional(CONF_TIME_ZONE, default=hass.config.time_zone): SelectSelector(
SelectSelectorConfig(
options=sorted(zoneinfo.available_timezones()),
)
SelectSelectorConfig(options=get_timezones, sort=True)
),
}
)
@ -109,7 +110,7 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user",
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:
return self.async_show_form(
data_schema=self.add_suggested_values_to_schema(
_get_data_schema(self.hass),
await _get_data_schema(self.hass),
reconfigure_entry.data,
),
step_id="reconfigure",

View File

@ -145,7 +145,10 @@ class KrakenData:
await asyncio.sleep(CALL_RATE_LIMIT_SLEEP)
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:
"""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]
for platform, discover_id in mqtt_data.discovery_already_discovered:
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)
if not ids:
continue

View File

@ -371,6 +371,9 @@ def migrate_entity_ids(
new_device_id = f"{host.unique_id}"
else:
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)}
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)}"
else:
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)}
existing_device = device_reg.async_get_device(identifiers=new_identifiers)
if existing_device is None:
@ -415,6 +421,11 @@ def migrate_entity_ids(
host.unique_id
):
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)
if entity.device_id in ch_device_ids:
@ -430,6 +441,11 @@ def migrate_entity_ids(
continue
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]}"
_LOGGER.debug(
"Updating Reolink entity unique_id from %s to %s",
entity.unique_id,
new_id,
)
existing_entity = entity_reg.async_get_entity_id(
entity.domain, entity.platform, new_id
)

View File

@ -19,5 +19,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"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)
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:
return VodRequestType.DOWNLOAD
return VodRequestType.PLAYBACK

View File

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

View File

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

View File

@ -200,8 +200,18 @@ def get_device_uptime(uptime: float, last_uptime: datetime | None) -> datetime:
if (
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 last_uptime

View File

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

View File

@ -23,7 +23,6 @@ from .entity import SmartThingsEntity
MEDIA_PLAYER_CAPABILITIES = (
Capability.AUDIO_MUTE,
Capability.AUDIO_VOLUME,
Capability.MEDIA_PLAYBACK,
)
CONTROLLABLE_SOURCES = ["bluetooth", "wifi"]
@ -100,27 +99,25 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
)
def _determine_features(self) -> MediaPlayerEntityFeature:
flags = MediaPlayerEntityFeature(0)
playback_commands = self.get_attribute_value(
Capability.MEDIA_PLAYBACK, Attribute.SUPPORTED_PLAYBACK_COMMANDS
flags = (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
)
if "play" in playback_commands:
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.AUDIO_VOLUME):
flags |= (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP
if self.supports_capability(Capability.MEDIA_PLAYBACK):
playback_commands = self.get_attribute_value(
Capability.MEDIA_PLAYBACK, Attribute.SUPPORTED_PLAYBACK_COMMANDS
)
if self.supports_capability(Capability.AUDIO_MUTE):
flags |= MediaPlayerEntityFeature.VOLUME_MUTE
if "play" in playback_commands:
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):
flags |= (
MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF
@ -270,6 +267,13 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
def state(self) -> MediaPlayerState | None:
"""State of the media player."""
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.source is not None

View File

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

View File

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

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/smhi",
"iot_class": "cloud_polling",
"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:
"""Build item for App or radio category."""
return BrowseMedia(
media_content_id=item.get("id", ""),
media_content_id=item["id"],
title=item["title"],
media_content_type=cmd,
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."""
return BrowseMedia(
media_content_id=item.get("id", ""),
media_content_id=item["id"],
title=item["title"],
media_content_type=search_type,
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"]:
return BrowseMedia(
media_content_id=item.get("id", ""),
media_content_id=item["id"],
title=item["title"],
media_content_type="Favorites",
media_class=CONTENT_TYPE_MEDIA_CLASS["Favorites"]["item"],
@ -193,7 +193,7 @@ def _build_response_favorites(item: dict[str, Any]) -> BrowseMedia:
can_play=False,
)
return BrowseMedia(
media_content_id=item.get("id", ""),
media_content_id=item["id"],
title=item["title"],
media_content_type="Favorites",
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)
elif item_type is not None:
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"]:
@ -263,6 +263,8 @@ async def build_item_response(
children = []
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":
child_media = _build_response_favorites(item)
@ -294,7 +296,7 @@ async def build_item_response(
elif item_type:
child_media = BrowseMedia(
media_content_id=str(item.get("id", "")),
media_content_id=item["id"],
title=item["title"],
media_content_type=item_type,
media_class=CONTENT_TYPE_MEDIA_CLASS[item_type]["item"],

View File

@ -113,7 +113,9 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = (
translation_key="last_boot_time",
device_class=SensorDeviceClass.TIMESTAMP,
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(
key="ping_drop_rate",

View File

@ -6,7 +6,12 @@ import logging
from aiohttp import ClientError, ClientResponseError
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 tessie_api import get_state_of_all_vehicles
@ -124,12 +129,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
continue
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(
TessieEnergyData(
api=api,
id=site_id,
live_coordinator=TessieEnergySiteLiveCoordinator(
hass, entry, api
live_coordinator=(
TessieEnergySiteLiveCoordinator(
hass, entry, api, live_status
)
if isinstance(live_status, dict)
else None
),
info_coordinator=TessieEnergySiteInfoCoordinator(
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()
for energysite in energysites
if energysite.live_coordinator is not None
),
*(
energysite.info_coordinator.async_config_entry_first_refresh()

View File

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

View File

@ -102,7 +102,11 @@ class TessieEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]):
config_entry: TessieConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: TessieConfigEntry, api: EnergySite
self,
hass: HomeAssistant,
config_entry: TessieConfigEntry,
api: EnergySite,
data: dict[str, Any],
) -> None:
"""Initialize Tessie Energy Site Live coordinator."""
super().__init__(
@ -114,6 +118,12 @@ class TessieEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]):
)
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]:
"""Update energy site data using Tessie API."""

View File

@ -41,7 +41,9 @@ async def async_get_config_entry_diagnostics(
]
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),
}
for x in entry.runtime_data.energysites

View File

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

View File

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

View File

@ -396,12 +396,16 @@ async def async_setup_entry(
TessieEnergyLiveSensorEntity(energysite, description)
for energysite in entry.runtime_data.energysites
for description in ENERGY_LIVE_DESCRIPTIONS
if description.key in energysite.live_coordinator.data
or description.key == "percentage_charged"
if energysite.live_coordinator is not None
and (
description.key in energysite.live_coordinator.data
or description.key == "percentage_charged"
)
),
( # Add wall connectors
TessieWallConnectorSensorEntity(energysite, din, description)
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 description in WALL_CONNECTOR_DESCRIPTIONS
),
@ -446,6 +450,7 @@ class TessieEnergyLiveSensorEntity(TessieEnergyEntity, SensorEntity):
) -> None:
"""Initialize the sensor."""
self.entity_description = description
assert data.live_coordinator is not None
super().__init__(data, data.live_coordinator, description.key)
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)
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)
self.update = Throttle(interval)(self._update)
@ -226,7 +226,7 @@ class UkTransportLiveTrainTimeSensor(UkTransportSensor):
self._next_trains = []
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__(
self, sensor_name, api_app_id, api_app_key, query_url

View File

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

View File

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

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 4
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
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."
)
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
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(DYNAMIC_CONTEXT_PROMPT)
return prompt
@callback
@ -395,7 +410,7 @@ class AssistAPI(API):
if exposed_entities and exposed_entities["entities"]:
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())))
@ -457,7 +472,7 @@ class AssistAPI(API):
)
if exposed_domains:
tools.append(GetHomeStateTool())
tools.append(GetLiveContextTool())
return tools
@ -898,7 +913,7 @@ class CalendarGetEventsTool(Tool):
return {"success": True, "result": events}
class GetHomeStateTool(Tool):
class GetLiveContextTool(Tool):
"""Tool for getting the current state of exposed entities.
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.
"""
name = "get_home_state"
description = "Get the current state of all devices in the home. "
name = "GetLiveContext"
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(
self,
@ -925,7 +945,7 @@ class GetHomeStateTool(Tool):
if not exposed_entities["entities"]:
return {"success": False, "error": NO_ENTITIES_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())),
]
return {

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.4.2"
version = "2025.4.3"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
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
# homeassistant.components.devolo_home_network
devolo-plc-api==1.4.1
devolo-plc-api==1.5.1
# homeassistant.components.chacon_dio
dio-chacon-wifi-api==1.2.1
@ -871,7 +871,7 @@ enocean==0.50
enturclient==0.2.4
# homeassistant.components.environment_canada
env-canada==0.8.0
env-canada==0.10.1
# homeassistant.components.season
ephem==4.1.6
@ -1154,7 +1154,7 @@ hole==0.8.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.69
holidays==0.70
# homeassistant.components.frontend
home-assistant-frontend==20250411.0
@ -2325,7 +2325,7 @@ pysmartthings==3.0.4
pysmarty2==0.10.2
# homeassistant.components.smhi
pysmhi==1.0.1
pysmhi==1.0.2
# homeassistant.components.edl21
pysml==0.0.12
@ -2627,7 +2627,7 @@ renault-api==0.2.9
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.13.1
reolink-aio==0.13.2
# homeassistant.components.idteck_prox
rfk101py==0.0.1
@ -3152,7 +3152,7 @@ zeroconf==0.146.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.55
zha==0.0.56
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

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

View File

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

View File

@ -33,7 +33,7 @@ async def init_integration(hass: HomeAssistant, ec_data) -> MockConfigEntry:
config_entry.add_to_hass(hass)
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.alerts = ec_data["alerts"]
weather_mock.daily_forecasts = ec_data["daily_forecasts"]

View File

@ -4,6 +4,7 @@ import contextlib
from datetime import datetime
import json
from env_canada.ec_weather import MetaData
import pytest
from tests.common import load_fixture
@ -13,7 +14,7 @@ from tests.common import load_fixture
def ec_data():
"""Load Environment Canada data."""
def date_hook(weather):
def data_hook(weather):
"""Convert timestamp string to datetime."""
if t := weather.get("timestamp"):
@ -22,9 +23,11 @@ def ec_data():
elif t := weather.get("period"):
with contextlib.suppress(ValueError):
weather["period"] = datetime.fromisoformat(t)
if t := weather.get("metadata"):
weather["metadata"] = MetaData(**t)
return weather
return json.loads(
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.lon = FAKE_CONFIG[CONF_LONGITUDE]
ec_mock.language = FAKE_CONFIG[CONF_LANGUAGE]
ec_mock.metadata = {"location": FAKE_TITLE}
ec_mock.metadata.location = FAKE_TITLE
ec_mock.update = AsyncMock()

View File

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

View File

@ -624,6 +624,49 @@
"isDue": false,
"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": {
"m": false,

View File

@ -66,7 +66,8 @@
"564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
"f2c85972-1a19-4426-bc6d-ce3337b9d99f",
"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"]
},

View File

@ -87,6 +87,20 @@
'summary': 'Fitnessstudio besuchen',
'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({
'description': 'Klicke um Änderungen zu machen!',
'end': dict({
@ -563,6 +577,20 @@
'summary': 'Fitnessstudio besuchen',
'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({
'description': 'Klicke um Änderungen zu machen!',
'end': dict({

View File

@ -1193,6 +1193,81 @@
]),
'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({
'alias': None,
'attribute': 'str',
@ -3465,6 +3540,81 @@
]),
'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({
'alias': None,
'attribute': 'str',
@ -4608,6 +4758,81 @@
]),
'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({
'alias': None,
'attribute': 'str',
@ -5199,6 +5424,81 @@
]),
'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({
'alias': None,
'attribute': 'str',

View File

@ -49,6 +49,13 @@
'summary': 'Arbeite an einem kreativen Projekt',
'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({
'description': 'Wähle eine Programmiersprache aus, die du noch nicht kennst, und lerne die Grundlagen.',
'status': 'needs_action',
@ -151,7 +158,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '4',
'state': '5',
})
# ---
# 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
async def test_correct_config_discovery(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
@pytest.mark.parametrize(
("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:
"""Test sending in correct JSON."""
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(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic" }',
discovery_topic,
json.dumps(config_init),
)
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.name == "Beer"
assert ("binary_sensor", "bla") in hass.data["mqtt"].discovery_already_discovered
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 = {
"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(

View File

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

View File

@ -23,15 +23,21 @@ from homeassistant.components.number import (
DOMAIN as NUMBER_DOMAIN,
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.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
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
DEV_ID_NVR = f"{TEST_UID}_{TEST_UID_CAM}"
DEV_ID_STANDALONE_CAM = f"{TEST_UID_CAM}"
@pytest.mark.parametrize(
("side_effect", "expected"),
@ -123,3 +129,36 @@ async def test_try_function(
assert err.value.translation_key == expected.translation_key
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,
GEN2_BETA_RELEASE_URL,
GEN2_RELEASE_URL,
UPTIME_DEVIATION,
)
from homeassistant.components.shelly.utils import (
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"))
assert get_device_uptime(
50, 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:10+00:00"))
55 - UPTIME_DEVIATION,
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(

View File

@ -107,7 +107,9 @@ def mock_smartthings() -> Generator[AsyncMock]:
"centralite",
"da_ref_normal_000001",
"da_ref_normal_01011",
"da_ref_normal_01001",
"vd_network_audio_002s",
"vd_network_audio_003s",
"vd_sensor_light_2023",
"iphone",
"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',
})
# ---
# 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]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -187,3 +187,50 @@
'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,
})
# ---
# 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]
DeviceRegistryEntrySnapshot({
'area_id': None,
@ -1652,6 +1685,39 @@
'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]
DeviceRegistryEntrySnapshot({
'area_id': None,

View File

@ -231,6 +231,56 @@
'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]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -4049,6 +4049,283 @@
'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]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -93,6 +93,53 @@
'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]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -181,13 +181,13 @@ async def test_assist_api(
assert len(llm.async_get_apis(hass)) == 1
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
intent_handler.platforms = None
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
intent_handler.platforms = {"light"}
@ -575,7 +575,7 @@ async def test_assist_api_prompt(
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
domain: light
state: 'on'
@ -623,7 +623,7 @@ async def test_assist_api_prompt(
state: unavailable
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
domain: light
- 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, "
"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)
assert api.api_prompt == (
f"""{first_part_prompt}
{area_prompt}
{no_timer_prompt}
{dynamic_context_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(
llm.ToolInput(tool_name="get_home_state", tool_args={})
llm.ToolInput(tool_name="GetLiveContext", tool_args={})
)
assert result == {
"success": True,
@ -697,6 +710,7 @@ async def test_assist_api_prompt(
f"""{first_part_prompt}
{area_prompt}
{no_timer_prompt}
{dynamic_context_prompt}
{stateless_exposed_entities_prompt}"""
)
@ -712,6 +726,7 @@ async def test_assist_api_prompt(
f"""{first_part_prompt}
{area_prompt}
{no_timer_prompt}
{dynamic_context_prompt}
{stateless_exposed_entities_prompt}"""
)
@ -723,6 +738,7 @@ async def test_assist_api_prompt(
assert api.api_prompt == (
f"""{first_part_prompt}
{area_prompt}
{dynamic_context_prompt}
{stateless_exposed_entities_prompt}"""
)