* Don't allow creating backups if Home Assistant is not running (#139499)

* Don't allow creating backups if hass is not running

* Revert "Don't allow creating backups if hass is not running"

This reverts commit 1bf545eb25f20fc27fe161691a94531cba7e005c.

* Set backup manager to idle only after Home Assistant has started

* Update according to discussion, add tests

* Add more test

* Bump govee_ble to 0.43.1 (#139862)

Bump govee_ble to 0.43.0

* Label emergency heat switch (#139872)

* Add label to emergency heat switch

* Use sentence case names

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

* Bump sense-energy lib to 0.13.7 (#140068)

* Update jinja to 3.1.6 (#140069)

* Update evohome-async to 1.0.3 (#140083)

bump client to 1.0.3

* Fix HEOS discovery error when previously ignored (#140091)

Abort ignored discovery

* Map prewash job state in SmartThings (#140097)

* Check support for thermostat operating state in SmartThings (#140103)

* Handle None options in SmartThings (#140110)

* Handle None options in SmartThings

* Handle None options in SmartThings

* Fix MQTT JSON light not reporting color temp status if color is not supported (#140113)

* Fix HEOS user initiated setup when discovery is waiting confirmation (#140119)

* Support null supported Thermostat modes in SmartThings (#140101)

* Set device class for Oven Completion time in SmartThings (#140139)

* Revert "Check if the unit of measurement is valid before creating the entity" (#140155)

Revert "Check if the unit of measurement is valid before creating the entity …"

This reverts commit 99e1a7a676b2fc14f9f8a8db64bee2840fae4646.

* Fix the order of the group members attribute of the Music Assistant integration (#140204)

* Fix events without user in Bring integration (#140213)

Fix events without publicUserUuid

* Log broad exception in Electricity Maps config flow (#140219)

* Bump evohome-async to 1.0.4 to fix  #140194 (#140230)

bump client, add test for fix  #140194

* Refresh Home Connect token during config entry setup (#140233)

* Refresh token during config entry setup

* Test 500 error

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Add 900 RPM option to washer spin speed options at Home Connect (#140234)

Add 900 RPM option to washer spin speed options

* Fix todo tool broken with Gemini 2.0 models. (#140246)

* Change tool name for addlist item

* Change to HasListAddItem

* extract to function

* Fix version not always available in onewire (#140260)

* Fix `client_id` not generated when connecting to the MQTT broker (#140264)

Fix client_id not generated when connecting to the MQTT broker

* Bump velbusaio to 2025.3.0 (#140267)

* Fix dryer operating state in SmartThings (#140277)

* FGLair : Upgrade to ayla-iot-unofficial 1.4.7 (#140296)

Upgrade to ayla-iot-unofficial 1.4.7

* Bump pyheos to v1.0.3 (#140310)

Bump pyheos v1.0.3

* Bump ZHA to 0.0.52 (#140325)

* Bump pydrawise to 2025.3.0 (#140330)

* Bump teslemetry-stream (#140335)

Bump

* Fix no temperature unit in SmartThings (#140363)

* Fix double space quoting in WebDAV (#140364)

* Bump python-roborock to 2.12.2 (#140368)

bump python roboorck to 2.12.2

* Handle incomplete power consumption reports in SmartThings (#140370)

* Fix browsing Audible Favorites in Sonos (#140378)

* initial commit

* updates

* update test data

* Make sure SmartThings light can deal with unknown states (#140190)

* Fix

* add comment

* Make light unknown

* Make light unknown

* Delete subscription on shutdown of SmartThings (#140135)

* Cache subscription url in SmartThings

* Cache subscription url in SmartThings

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Bump pysmartthings to 2.7.1

* 2.7.2

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Bump version to 2025.3.2

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Evan Farrell <evan@evanfarrell.com>
Co-authored-by: John Hillery <34005807+jrhillery@users.noreply.github.com>
Co-authored-by: Keilin Bickar <TrumpetGod@gmail.com>
Co-authored-by: David Bonnes <zxdavb@bonnes.me>
Co-authored-by: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: msm595 <msm595@users.noreply.github.com>
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
Co-authored-by: J. Diego Rodríguez Royo <jdrr1998@hotmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Luke Lashley <conway220@gmail.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Maikel Punie <maikel.punie@gmail.com>
Co-authored-by: Antoine Reversat <a.reversat@gmail.com>
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
Co-authored-by: David Knowles <dknowles2@gmail.com>
Co-authored-by: Brett Adams <Bre77@users.noreply.github.com>
Co-authored-by: Pete Sage <76050312+PeteRager@users.noreply.github.com>
This commit is contained in:
Franck Nijhof 2025-03-11 17:36:00 +01:00 committed by GitHub
commit a12915fc14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
86 changed files with 4915 additions and 283 deletions

View File

@ -118,6 +118,7 @@ class BackupManagerState(StrEnum):
IDLE = "idle"
CREATE_BACKUP = "create_backup"
BLOCKED = "blocked"
RECEIVE_BACKUP = "receive_backup"
RESTORE_BACKUP = "restore_backup"
@ -226,6 +227,13 @@ class RestoreBackupEvent(ManagerStateEvent):
state: RestoreBackupState
@dataclass(frozen=True, kw_only=True, slots=True)
class BlockedEvent(ManagerStateEvent):
"""Backup manager blocked, Home Assistant is starting."""
manager_state: BackupManagerState = BackupManagerState.BLOCKED
class BackupPlatformProtocol(Protocol):
"""Define the format that backup platforms can have."""
@ -340,7 +348,7 @@ class BackupManager:
self.remove_next_delete_event: Callable[[], None] | None = None
# Latest backup event and backup event subscribers
self.last_event: ManagerStateEvent = IdleEvent()
self.last_event: ManagerStateEvent = BlockedEvent()
self.last_non_idle_event: ManagerStateEvent | None = None
self._backup_event_subscriptions = hass.data[
DATA_BACKUP
@ -354,10 +362,19 @@ class BackupManager:
self.known_backups.load(stored["backups"])
await self._reader_writer.async_validate_config(config=self.config)
await self._reader_writer.async_resume_restore_progress_after_restart(
on_progress=self.async_on_backup_event
)
async def set_manager_idle_after_start(hass: HomeAssistant) -> None:
"""Set manager to idle after start."""
self.async_on_backup_event(IdleEvent())
if self.state == BackupManagerState.BLOCKED:
# If we're not finishing a restore job, set the manager to idle after start
start.async_at_started(self.hass, set_manager_idle_after_start)
await self.load_platforms()
@property
@ -1293,7 +1310,7 @@ class BackupManager:
if (current_state := self.state) != (new_state := event.manager_state):
LOGGER.debug("Backup state: %s -> %s", current_state, new_state)
self.last_event = event
if not isinstance(event, IdleEvent):
if not isinstance(event, (BlockedEvent, IdleEvent)):
self.last_non_idle_event = event
for subscription in self._backup_event_subscriptions:
subscription(event)

View File

@ -77,9 +77,12 @@ class BringEventEntity(BringBaseEntity, EventEntity):
attributes = asdict(activity.content)
attributes["last_activity_by"] = next(
x.name
for x in bring_list.users.users
if x.publicUuid == activity.content.publicUserUuid
(
x.name
for x in bring_list.users.users
if x.publicUuid == activity.content.publicUserUuid
),
None,
)
self._trigger_event(

View File

@ -3,11 +3,11 @@
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from aioelectricitymaps import (
ElectricityMaps,
ElectricityMapsError,
ElectricityMapsInvalidTokenError,
ElectricityMapsNoDataError,
)
@ -36,6 +36,8 @@ TYPE_USE_HOME = "use_home_location"
TYPE_SPECIFY_COORDINATES = "specify_coordinates"
TYPE_SPECIFY_COUNTRY = "specify_country_code"
_LOGGER = logging.getLogger(__name__)
class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Co2signal."""
@ -158,7 +160,8 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "invalid_auth"
except ElectricityMapsNoDataError:
errors["base"] = "no_data"
except ElectricityMapsError:
except Exception:
_LOGGER.exception("Unexpected error occurred while checking API key")
errors["base"] = "unknown"
else:
if self.source == SOURCE_REAUTH:

View File

@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.13.6"]
"requirements": ["sense-energy==0.13.7"]
}

View File

@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==1.0.2"]
"requirements": ["evohome-async==1.0.4"]
}

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair",
"iot_class": "cloud_polling",
"requirements": ["ayla-iot-unofficial==1.4.5"]
"requirements": ["ayla-iot-unofficial==1.4.7"]
}

View File

@ -276,6 +276,13 @@ class GoogleGenerativeAIConversationEntity(
):
return await self._async_handle_message(user_input, chat_log)
def _fix_tool_name(self, tool_name: str) -> str:
"""Fix tool name if needed."""
# The Gemini 2.0+ tokenizer seemingly has a issue with the HassListAddItem tool
# name. This makes sure when it incorrectly changes the name, that we change it
# back for HA to call.
return tool_name if tool_name != "HasListAddItem" else "HassListAddItem"
async def _async_handle_message(
self,
user_input: conversation.ConversationInput,
@ -435,7 +442,10 @@ class GoogleGenerativeAIConversationEntity(
tool_name = tool_call.name
tool_args = _escape_decode(tool_call.args)
tool_calls.append(
llm.ToolInput(tool_name=tool_name, tool_args=tool_args)
llm.ToolInput(
tool_name=self._fix_tool_name(tool_name),
tool_args=tool_args,
)
)
chat_request = _create_google_tool_response_content(

View File

@ -135,5 +135,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/govee_ble",
"iot_class": "local_push",
"requirements": ["govee-ble==0.43.0"]
"requirements": ["govee-ble==0.43.1"]
}

View File

@ -14,7 +14,12 @@ from pyheos import (
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.config_entries import (
SOURCE_IGNORE,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import selector
@ -141,8 +146,10 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
hostname = urlparse(discovery_info.ssdp_location).hostname
assert hostname is not None
# Abort early when discovered host is part of the current system
if entry and hostname in _get_current_hosts(entry):
# Abort early when discovery is ignored or host is part of the current system
if entry and (
entry.source == SOURCE_IGNORE or hostname in _get_current_hosts(entry)
):
return self.async_abort(reason="single_instance_allowed")
# Connect to discovered host and get system information
@ -198,7 +205,7 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Obtain host and validate connection."""
await self.async_set_unique_id(DOMAIN)
await self.async_set_unique_id(DOMAIN, raise_on_progress=False)
self._abort_if_unique_id_configured(error="single_instance_allowed")
# Try connecting to host if provided
errors: dict[str, str] = {}

View File

@ -159,13 +159,12 @@ class HeosCoordinator(DataUpdateCoordinator[None]):
async def _async_on_reconnected(self) -> None:
"""Handle when reconnected so resources are updated and entities marked available."""
await self._async_update_players()
await self._async_update_sources()
_LOGGER.warning("Successfully reconnected to HEOS host %s", self.host)
self.async_update_listeners()
async def _async_on_controller_event(
self, event: str, data: PlayerUpdateResult | None
self, event: str, data: PlayerUpdateResult | None = None
) -> None:
"""Handle a controller event, such as players or groups changed."""
if event == const.EVENT_PLAYERS_CHANGED:

View File

@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["pyheos"],
"quality_scale": "platinum",
"requirements": ["pyheos==1.0.2"],
"requirements": ["pyheos==1.0.3"],
"ssdp": [
{
"st": "urn:schemas-denon-com:device:ACT-Denon:1"

View File

@ -16,11 +16,17 @@ from aiohomeconnect.model import (
SettingKey,
)
from aiohomeconnect.model.error import HomeConnectError
import aiohttp
import voluptuous as vol
from homeassistant.const import ATTR_DEVICE_ID, Platform
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
ServiceValidationError,
)
from homeassistant.helpers import (
config_entry_oauth2_flow,
config_validation as cv,
@ -611,6 +617,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeConnectConfigEntry)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
config_entry_auth = AsyncConfigEntryAuth(hass, session)
try:
await config_entry_auth.async_get_access_token()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
home_connect_client = HomeConnectClient(config_entry_auth)

View File

@ -285,6 +285,7 @@ SPIN_SPEED_OPTIONS = {
"LaundryCare.Washer.EnumType.SpinSpeed.RPM400",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM600",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM800",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM900",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1000",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1200",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1400",

View File

@ -461,6 +461,7 @@
"laundry_care_washer_enum_type_spin_speed_r_p_m400": "400 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m600": "600 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "800 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m900": "900 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "1000 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "1200 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "1400 rpm",
@ -1430,6 +1431,7 @@
"laundry_care_washer_enum_type_spin_speed_r_p_m400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m400%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m600%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m800%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m900": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m900%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1000%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1200%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1400%]",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2025.2.0"]
"requirements": ["pydrawise==2025.3.0"]
}

View File

@ -15,6 +15,7 @@ import socket
import ssl
import time
from typing import TYPE_CHECKING, Any
from uuid import uuid4
import certifi
@ -292,7 +293,7 @@ class MqttClientSetup:
"""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
from paho.mqtt import client as mqtt # pylint: disable=import-outside-toplevel
# pylint: disable-next=import-outside-toplevel
from .async_client import AsyncMQTTClient
@ -309,9 +310,10 @@ class MqttClientSetup:
clean_session = True
if (client_id := config.get(CONF_CLIENT_ID)) is None:
# PAHO MQTT relies on the MQTT server to generate random client IDs.
# However, that feature is not mandatory so we generate our own.
client_id = None
# PAHO MQTT relies on the MQTT server to generate random client ID
# for protocol version 3.1, however, that feature is not mandatory
# so we generate our own.
client_id = mqtt._base62(uuid4().int, padding=22) # noqa: SLF001
transport: str = config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT)
self._client = AsyncMQTTClient(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,

View File

@ -31,7 +31,6 @@ from homeassistant.components.light import (
LightEntity,
LightEntityFeature,
brightness_supported,
color_supported,
valid_supported_color_modes,
)
from homeassistant.const import (
@ -293,7 +292,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
elif values["state"] is None:
self._attr_is_on = None
if color_supported(self.supported_color_modes) and "color_mode" in values:
if "color_mode" in values:
self._update_color(values)
if brightness_supported(self.supported_color_modes):

View File

@ -11,7 +11,6 @@ import voluptuous as vol
from homeassistant.components import sensor
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASS_UNITS,
DEVICE_CLASSES_SCHEMA,
ENTITY_ID_FORMAT,
STATE_CLASSES_SCHEMA,
@ -108,20 +107,6 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
)
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is None:
return config
if (
device_class in DEVICE_CLASS_UNITS
and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class]
):
raise vol.Invalid(
f"The unit of measurement `{unit_of_measurement}` is not valid "
f"together with device class `{device_class}`"
)
return config

View File

@ -276,22 +276,26 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
self._attr_state = MediaPlayerState(player.state.value)
else:
self._attr_state = MediaPlayerState(STATE_OFF)
group_members_entity_ids: list[str] = []
group_members: list[str] = []
if player.group_childs:
# translate MA group_childs to HA group_members as entity id's
entity_registry = er.async_get(self.hass)
group_members_entity_ids = [
entity_id
for child_id in player.group_childs
if (
entity_id := entity_registry.async_get_entity_id(
self.platform.domain, DOMAIN, child_id
)
group_members = player.group_childs
elif player.synced_to and (parent := self.mass.players.get(player.synced_to)):
group_members = parent.group_childs
# translate MA group_childs to HA group_members as entity id's
entity_registry = er.async_get(self.hass)
group_members_entity_ids: list[str] = [
entity_id
for child_id in group_members
if (
entity_id := entity_registry.async_get_entity_id(
self.platform.domain, DOMAIN, child_id
)
]
# NOTE: we sort the group_members for now,
# until the MA API returns them sorted (group_childs is now a set)
self._attr_group_members = sorted(group_members_entity_ids)
)
]
self._attr_group_members = group_members_entity_ids
self._attr_volume_level = (
player.volume_level / 100 if player.volume_level is not None else None
)

View File

@ -58,6 +58,9 @@
"switch": {
"hold": {
"name": "Hold"
},
"emergency_heat": {
"name": "Emergency heat"
}
}
},

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import contextlib
from datetime import datetime, timedelta
import logging
import os
@ -58,7 +59,7 @@ class OneWireHub:
owproxy: protocol._Proxy
devices: list[OWDeviceDescription]
_version: str
_version: str | None = None
def __init__(self, hass: HomeAssistant, config_entry: OneWireConfigEntry) -> None:
"""Initialize."""
@ -74,7 +75,9 @@ class OneWireHub:
port = self._config_entry.data[CONF_PORT]
_LOGGER.debug("Initializing connection to %s:%s", host, port)
self.owproxy = protocol.proxy(host, port)
self._version = self.owproxy.read(protocol.PTH_VERSION).decode()
with contextlib.suppress(protocol.OwnetError):
# Version is not available on all servers
self._version = self.owproxy.read(protocol.PTH_VERSION).decode()
self.devices = _discover_devices(self.owproxy)
async def initialize(self) -> None:

View File

@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": [
"python-roborock==2.11.1",
"python-roborock==2.12.2",
"vacuum-map-parser-roborock==0.1.2"
]
}

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/sense",
"iot_class": "cloud_polling",
"loggers": ["sense_energy"],
"requirements": ["sense-energy==0.13.6"]
"requirements": ["sense-energy==0.13.7"]
}

View File

@ -16,12 +16,18 @@ from pysmartthings import (
Scene,
SmartThings,
SmartThingsAuthenticationFailedError,
SmartThingsSinkError,
Status,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_TOKEN,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -33,6 +39,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
from .const import (
CONF_INSTALLED_APP_ID,
CONF_LOCATION_ID,
CONF_SUBSCRIPTION_ID,
DOMAIN,
EVENT_BUTTON,
MAIN,
@ -99,6 +106,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
client.refresh_token_function = _refresh_token
def _handle_max_connections() -> None:
_LOGGER.debug("We hit the limit of max connections")
hass.config_entries.async_schedule_reload(entry.entry_id)
client.max_connections_reached_callback = _handle_max_connections
def _handle_new_subscription_identifier(identifier: str | None) -> None:
"""Handle a new subscription identifier."""
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_SUBSCRIPTION_ID: identifier,
},
)
if identifier is not None:
_LOGGER.debug("Updating subscription ID to %s", identifier)
else:
_LOGGER.debug("Removing subscription ID")
client.new_subscription_id_callback = _handle_new_subscription_identifier
if (old_identifier := entry.data.get(CONF_SUBSCRIPTION_ID)) is not None:
_LOGGER.debug("Trying to delete old subscription %s", old_identifier)
await client.delete_subscription(old_identifier)
_LOGGER.debug("Trying to create a new subscription")
try:
subscription = await client.create_subscription(
entry.data[CONF_LOCATION_ID],
entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID],
)
except SmartThingsSinkError as err:
_LOGGER.debug("Couldn't create a new subscription: %s", err)
raise ConfigEntryNotReady from err
subscription_id = subscription.subscription_id
_handle_new_subscription_identifier(subscription_id)
entry.async_create_background_task(
hass,
client.subscribe(
entry.data[CONF_LOCATION_ID],
entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID],
subscription,
),
"smartthings_socket",
)
device_status: dict[str, FullDevice] = {}
try:
devices = await client.get_devices()
@ -145,12 +200,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
client.add_unspecified_device_event_listener(handle_button_press)
)
entry.async_create_background_task(
hass,
client.subscribe(
entry.data[CONF_LOCATION_ID], entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID]
),
"smartthings_webhook",
async def _handle_shutdown(_: Event) -> None:
"""Handle shutdown."""
await client.delete_subscription(subscription_id)
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _handle_shutdown)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -176,6 +231,9 @@ async def async_unload_entry(
hass: HomeAssistant, entry: SmartThingsConfigEntry
) -> bool:
"""Unload a config entry."""
client = entry.runtime_data.client
if (subscription_id := entry.data.get(CONF_SUBSCRIPTION_ID)) is not None:
await client.delete_subscription(subscription_id)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@ -194,34 +252,15 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
KEEP_CAPABILITY_QUIRK: dict[
Capability | str, Callable[[dict[Attribute | str, Status]], bool]
] = {
Capability.DRYER_OPERATING_STATE: (
lambda status: status[Attribute.SUPPORTED_MACHINE_STATES].value is not None
),
Capability.WASHER_OPERATING_STATE: (
lambda status: status[Attribute.SUPPORTED_MACHINE_STATES].value is not None
),
Capability.DEMAND_RESPONSE_LOAD_CONTROL: lambda _: True,
}
POWER_CONSUMPTION_FIELDS = {
"energy",
"power",
"deltaEnergy",
"powerEnergy",
"energySaved",
}
CAPABILITY_VALIDATION: dict[
Capability | str, Callable[[dict[Attribute | str, Status]], bool]
] = {
Capability.POWER_CONSUMPTION_REPORT: (
lambda status: (
(power_consumption := status[Attribute.POWER_CONSUMPTION].value) is not None
and all(
field in cast(dict, power_consumption)
for field in POWER_CONSUMPTION_FIELDS
)
)
)
}
def process_status(
status: dict[str, dict[Capability | str, dict[Attribute | str, Status]]],
@ -245,8 +284,4 @@ def process_status(
or not KEEP_CAPABILITY_QUIRK[capability](main_component[capability])
):
del main_component[capability]
for capability in list(main_component):
if capability in CAPABILITY_VALIDATION:
if not CAPABILITY_VALIDATION[capability](main_component[capability]):
del main_component[capability]
return status

View File

@ -251,6 +251,8 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
@property
def hvac_action(self) -> HVACAction | None:
"""Return the current running hvac operation if supported."""
if not self.supports_capability(Capability.THERMOSTAT_OPERATING_STATE):
return None
return OPERATING_STATE_TO_ACTION.get(
self.get_attribute_value(
Capability.THERMOSTAT_OPERATING_STATE,
@ -270,11 +272,15 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available operation modes."""
return [
state
for mode in self.get_attribute_value(
if (
supported_thermostat_modes := self.get_attribute_value(
Capability.THERMOSTAT_MODE, Attribute.SUPPORTED_THERMOSTAT_MODES
)
) is None:
return []
return [
state
for mode in supported_thermostat_modes
if (state := AC_MODE_TO_STATE.get(mode)) is not None
]
@ -312,10 +318,14 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
unit = self._internal_state[Capability.TEMPERATURE_MEASUREMENT][
Attribute.TEMPERATURE
].unit
assert unit
# Offline third party thermostats may not have a unit
# Since climate always requires a unit, default to Celsius
if (
unit := self._internal_state[Capability.TEMPERATURE_MEASUREMENT][
Attribute.TEMPERATURE
].unit
) is None:
return UnitOfTemperature.CELSIUS
return UNIT_MAP[unit]

View File

@ -33,4 +33,5 @@ CONF_REFRESH_TOKEN = "refresh_token"
MAIN = "main"
OLD_DATA = "old_data"
CONF_SUBSCRIPTION_ID = "subscription_id"
EVENT_BUTTON = "smartthings.button"

View File

@ -147,14 +147,21 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
"""Update entity attributes when the device status has changed."""
# Brightness and transition
if brightness_supported(self._attr_supported_color_modes):
self._attr_brightness = int(
convert_scale(
self.get_attribute_value(Capability.SWITCH_LEVEL, Attribute.LEVEL),
100,
255,
0,
if (
brightness := self.get_attribute_value(
Capability.SWITCH_LEVEL, Attribute.LEVEL
)
) is None:
self._attr_brightness = None
else:
self._attr_brightness = int(
convert_scale(
brightness,
100,
255,
0,
)
)
)
# Color Temperature
if ColorMode.COLOR_TEMP in self._attr_supported_color_modes:
self._attr_color_temp_kelvin = self.get_attribute_value(
@ -162,16 +169,21 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
)
# Color
if ColorMode.HS in self._attr_supported_color_modes:
self._attr_hs_color = (
convert_scale(
self.get_attribute_value(Capability.COLOR_CONTROL, Attribute.HUE),
100,
360,
),
self.get_attribute_value(
Capability.COLOR_CONTROL, Attribute.SATURATION
),
)
if (
hue := self.get_attribute_value(Capability.COLOR_CONTROL, Attribute.HUE)
) is None:
self._attr_hs_color = None
else:
self._attr_hs_color = (
convert_scale(
hue,
100,
360,
),
self.get_attribute_value(
Capability.COLOR_CONTROL, Attribute.SATURATION
),
)
async def async_set_color(self, hs_color):
"""Set the color of the device."""
@ -217,6 +229,10 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
super()._update_handler(event)
@property
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Return true if light is on."""
return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on"
if (
state := self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH)
) is None:
return None
return state == "on"

View File

@ -29,5 +29,5 @@
"documentation": "https://www.home-assistant.io/integrations/smartthings",
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"requirements": ["pysmartthings==2.7.0"]
"requirements": ["pysmartthings==2.7.2"]
}

View File

@ -5,9 +5,9 @@ from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from typing import Any, cast
from pysmartthings import Attribute, Capability, SmartThings
from pysmartthings import Attribute, Capability, SmartThings, Status
from homeassistant.components.sensor import (
SensorDeviceClass,
@ -57,6 +57,7 @@ JOB_STATE_MAP = {
"freezeProtection": "freeze_protection",
"preDrain": "pre_drain",
"preWash": "pre_wash",
"prewash": "pre_wash",
"wrinklePrevent": "wrinkle_prevent",
"unknown": None,
}
@ -130,6 +131,7 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
unique_id_separator: str = "."
capability_ignore_list: list[set[Capability]] | None = None
options_attribute: Attribute | None = None
exists_fn: Callable[[Status], bool] | None = None
CAPABILITY_TO_SENSORS: dict[
@ -560,6 +562,8 @@ CAPABILITY_TO_SENSORS: dict[
SmartThingsSensorEntityDescription(
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
)
],
},
@ -580,6 +584,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["energy"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "energy" in value
),
),
SmartThingsSensorEntityDescription(
key="power_meter",
@ -589,6 +597,10 @@ CAPABILITY_TO_SENSORS: dict[
value_fn=lambda value: value["power"],
extra_state_attributes_fn=power_attributes,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "power" in value
),
),
SmartThingsSensorEntityDescription(
key="deltaEnergy_meter",
@ -598,6 +610,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["deltaEnergy"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "deltaEnergy" in value
),
),
SmartThingsSensorEntityDescription(
key="powerEnergy_meter",
@ -607,6 +623,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["powerEnergy"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "powerEnergy" in value
),
),
SmartThingsSensorEntityDescription(
key="energySaved_meter",
@ -616,6 +636,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["energySaved"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "energySaved" in value
),
),
]
},
@ -970,6 +994,10 @@ async def async_setup_entry(
for capability_list in description.capability_ignore_list
)
)
and (
not description.exists_fn
or description.exists_fn(device.status[MAIN][capability][attribute])
)
)
@ -1022,8 +1050,11 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
def options(self) -> list[str] | None:
"""Return the options for this sensor."""
if self.entity_description.options_attribute:
options = self.get_attribute_value(
self.capability, self.entity_description.options_attribute
)
if (
options := self.get_attribute_value(
self.capability, self.entity_description.options_attribute
)
) is None:
return []
return [option.lower() for option in options]
return super().options

View File

@ -32,6 +32,7 @@ SONOS_TRACKS = "tracks"
SONOS_COMPOSER = "composers"
SONOS_RADIO = "radio"
SONOS_OTHER_ITEM = "other items"
SONOS_AUDIO_BOOK = "audio book"
SONOS_STATE_PLAYING = "PLAYING"
SONOS_STATE_TRANSITIONING = "TRANSITIONING"
@ -67,6 +68,7 @@ SONOS_TO_MEDIA_CLASSES = {
"object.item": MediaClass.TRACK,
"object.item.audioItem.musicTrack": MediaClass.TRACK,
"object.item.audioItem.audioBroadcast": MediaClass.GENRE,
"object.item.audioItem.audioBook": MediaClass.TRACK,
}
SONOS_TO_MEDIA_TYPES = {
@ -84,6 +86,7 @@ SONOS_TO_MEDIA_TYPES = {
"object.container.playlistContainer.sameArtist": MediaType.ARTIST,
"object.container.playlistContainer": MediaType.PLAYLIST,
"object.item.audioItem.musicTrack": MediaType.TRACK,
"object.item.audioItem.audioBook": MediaType.TRACK,
}
MEDIA_TYPES_TO_SONOS: dict[MediaType | str, str] = {
@ -113,6 +116,7 @@ SONOS_TYPES_MAPPING = {
"object.item": SONOS_OTHER_ITEM,
"object.item.audioItem.musicTrack": SONOS_TRACKS,
"object.item.audioItem.audioBroadcast": SONOS_RADIO,
"object.item.audioItem.audioBook": SONOS_AUDIO_BOOK,
}
LIBRARY_TITLES_MAPPING = {

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==0.9.12", "teslemetry-stream==0.6.10"]
"requirements": ["tesla-fleet-api==0.9.12", "teslemetry-stream==0.6.12"]
}

View File

@ -14,7 +14,7 @@
"velbus-protocol"
],
"quality_scale": "bronze",
"requirements": ["velbus-aio==2025.1.1"],
"requirements": ["velbus-aio==2025.3.0"],
"usb": [
{
"vid": "10CF",

View File

@ -13,7 +13,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from .const import CONF_BACKUP_PATH, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .helpers import async_create_client, async_ensure_path_exists
from .helpers import (
async_create_client,
async_ensure_path_exists,
async_migrate_wrong_folder_path,
)
type WebDavConfigEntry = ConfigEntry[Client]
@ -46,10 +50,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: WebDavConfigEntry) -> bo
translation_key="cannot_connect",
)
path = entry.data.get(CONF_BACKUP_PATH, "/")
await async_migrate_wrong_folder_path(client, path)
# Ensure the backup directory exists
if not await async_ensure_path_exists(
client, entry.data.get(CONF_BACKUP_PATH, "/")
):
if not await async_ensure_path_exists(client, path):
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_access_or_create_backup_path",

View File

@ -1,10 +1,18 @@
"""Helper functions for the WebDAV component."""
import logging
from aiowebdav2.client import Client, ClientOptions
from aiowebdav2.exceptions import WebDavError
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@callback
def async_create_client(
@ -36,3 +44,24 @@ async def async_ensure_path_exists(client: Client, path: str) -> bool:
return False
return True
async def async_migrate_wrong_folder_path(client: Client, path: str) -> None:
"""Migrate the wrong encoded folder path to the correct one."""
wrong_path = path.replace(" ", "%20")
if await client.check(wrong_path):
try:
await client.move(wrong_path, path)
except WebDavError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="failed_to_migrate_folder",
translation_placeholders={
"wrong_path": wrong_path,
"correct_path": path,
},
) from err
_LOGGER.debug(
"Migrated wrong encoded folder path from %s to %s", wrong_path, path
)

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aiowebdav2"],
"quality_scale": "bronze",
"requirements": ["aiowebdav2==0.4.1"]
"requirements": ["aiowebdav2==0.4.2"]
}

View File

@ -36,6 +36,9 @@
},
"cannot_access_or_create_backup_path": {
"message": "Cannot access or create backup path. Please check the path and permissions."
},
"failed_to_migrate_folder": {
"message": "Failed to migrate wrong encoded folder \"{wrong_path}\" to \"{correct_path}\"."
}
}
}

View File

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

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "1"
PATCH_VERSION: Final = "2"
__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

@ -41,7 +41,7 @@ home-assistant-frontend==20250306.0
home-assistant-intents==2025.3.5
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.5
Jinja2==3.1.6
lru-dict==1.3.0
mutagen==1.47.0
orjson==3.10.12

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.3.1"
version = "2025.3.2"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@ -52,7 +52,7 @@ dependencies = [
"httpx==0.28.1",
"home-assistant-bluetooth==1.13.1",
"ifaddr==0.2.0",
"Jinja2==3.1.5",
"Jinja2==3.1.6",
"lru-dict==1.3.0",
"PyJWT==2.10.1",
# PyJWT has loose dependency. We want the latest one.

2
requirements.txt generated
View File

@ -25,7 +25,7 @@ hass-nabucasa==0.92.0
httpx==0.28.1
home-assistant-bluetooth==1.13.1
ifaddr==0.2.0
Jinja2==3.1.5
Jinja2==3.1.6
lru-dict==1.3.0
PyJWT==2.10.1
cryptography==44.0.1

24
requirements_all.txt generated
View File

@ -422,7 +422,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webdav
aiowebdav2==0.4.1
aiowebdav2==0.4.2
# homeassistant.components.webostv
aiowebostv==0.7.3
@ -557,7 +557,7 @@ av==13.1.0
axis==64
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.5
ayla-iot-unofficial==1.4.7
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@ -899,7 +899,7 @@ eufylife-ble-client==0.1.8
# evdev==1.6.1
# homeassistant.components.evohome
evohome-async==1.0.2
evohome-async==1.0.4
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@ -1058,7 +1058,7 @@ goslide-api==0.7.0
gotailwind==0.3.0
# homeassistant.components.govee_ble
govee-ble==0.43.0
govee-ble==0.43.1
# homeassistant.components.govee_light_local
govee-local-api==2.0.1
@ -1906,7 +1906,7 @@ pydiscovergy==3.0.2
pydoods==1.0.2
# homeassistant.components.hydrawise
pydrawise==2025.2.0
pydrawise==2025.3.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0
@ -1996,7 +1996,7 @@ pygti==0.9.4
pyhaversion==22.8.0
# homeassistant.components.heos
pyheos==1.0.2
pyheos==1.0.3
# homeassistant.components.hive
pyhive-integration==1.0.2
@ -2310,7 +2310,7 @@ pysma==0.7.5
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartthings==2.7.0
pysmartthings==2.7.2
# homeassistant.components.smarty
pysmarty2==0.10.2
@ -2461,7 +2461,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==2.11.1
python-roborock==2.12.2
# homeassistant.components.smarttub
python-smarttub==0.0.39
@ -2694,7 +2694,7 @@ sendgrid==6.8.2
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense-energy==0.13.6
sense-energy==0.13.7
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1
@ -2881,7 +2881,7 @@ tesla-powerwall==0.5.2
tesla-wall-connector==1.0.2
# homeassistant.components.teslemetry
teslemetry-stream==0.6.10
teslemetry-stream==0.6.12
# homeassistant.components.tessie
tessie-api==0.1.1
@ -3000,7 +3000,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.2
# homeassistant.components.velbus
velbus-aio==2025.1.1
velbus-aio==2025.3.0
# homeassistant.components.venstar
venstarcolortouch==0.19
@ -3149,7 +3149,7 @@ zeroconf==0.145.1
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.51
zha==0.0.52
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@ -404,7 +404,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webdav
aiowebdav2==0.4.1
aiowebdav2==0.4.2
# homeassistant.components.webostv
aiowebostv==0.7.3
@ -506,7 +506,7 @@ av==13.1.0
axis==64
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.5
ayla-iot-unofficial==1.4.7
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@ -765,7 +765,7 @@ eternalegypt==0.0.16
eufylife-ble-client==0.1.8
# homeassistant.components.evohome
evohome-async==1.0.2
evohome-async==1.0.4
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@ -908,7 +908,7 @@ goslide-api==0.7.0
gotailwind==0.3.0
# homeassistant.components.govee_ble
govee-ble==0.43.0
govee-ble==0.43.1
# homeassistant.components.govee_light_local
govee-local-api==2.0.1
@ -1556,7 +1556,7 @@ pydexcom==0.2.3
pydiscovergy==3.0.2
# homeassistant.components.hydrawise
pydrawise==2025.2.0
pydrawise==2025.3.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0
@ -1625,7 +1625,7 @@ pygti==0.9.4
pyhaversion==22.8.0
# homeassistant.components.heos
pyheos==1.0.2
pyheos==1.0.3
# homeassistant.components.hive
pyhive-integration==1.0.2
@ -1882,7 +1882,7 @@ pysma==0.7.5
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartthings==2.7.0
pysmartthings==2.7.2
# homeassistant.components.smarty
pysmarty2==0.10.2
@ -1994,7 +1994,7 @@ python-picnic-api2==1.2.2
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==2.11.1
python-roborock==2.12.2
# homeassistant.components.smarttub
python-smarttub==0.0.39
@ -2173,7 +2173,7 @@ securetar==2025.2.1
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense-energy==0.13.6
sense-energy==0.13.7
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1
@ -2321,7 +2321,7 @@ tesla-powerwall==0.5.2
tesla-wall-connector==1.0.2
# homeassistant.components.teslemetry
teslemetry-stream==0.6.10
teslemetry-stream==0.6.12
# homeassistant.components.tessie
tessie-api==0.1.1
@ -2416,7 +2416,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.2
# homeassistant.components.velbus
velbus-aio==2025.1.1
velbus-aio==2025.3.0
# homeassistant.components.venstar
venstarcolortouch==0.19
@ -2538,7 +2538,7 @@ zeroconf==0.145.1
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.51
zha==0.0.52
# homeassistant.components.zwave_js
zwave-js-server-python==0.60.1

View File

@ -47,7 +47,8 @@ from homeassistant.components.backup.manager import (
WrittenBackup,
)
from homeassistant.components.backup.util import password_to_key
from homeassistant.core import HomeAssistant
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir
@ -3469,3 +3470,66 @@ async def test_restore_progress_after_restart_fail_to_remove(
"Unexpected error deleting backup restore result file: <class 'OSError'> Boom!"
in caplog.text
)
async def test_manager_blocked_until_home_assistant_started(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test backup manager's state is blocked until Home Assistant has started."""
hass.set_state(CoreState.not_running)
await setup_backup_integration(hass)
manager = hass.data[DATA_MANAGER]
assert manager.state == BackupManagerState.BLOCKED
assert manager.last_non_idle_event is None
# Fired when Home Assistant changes to starting state
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert manager.state == BackupManagerState.BLOCKED
assert manager.last_non_idle_event is None
# Fired when Home Assistant changes to running state
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert manager.state == BackupManagerState.IDLE
assert manager.last_non_idle_event is None
async def test_manager_not_blocked_after_restore(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test restore backup progress after restart."""
restore_result = {"error": None, "error_type": None, "success": True}
hass.set_state(CoreState.not_running)
with patch(
"pathlib.Path.read_bytes", return_value=json.dumps(restore_result).encode()
):
await setup_backup_integration(hass)
ws_client = await hass_ws_client(hass)
await ws_client.send_json_auto_id({"type": "backup/info"})
result = await ws_client.receive_json()
assert result["success"] is True
assert result["result"] == {
"agent_errors": {},
"backups": [],
"last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None,
"last_non_idle_event": {
"manager_state": "restore_backup",
"reason": None,
"stage": None,
"state": "completed",
},
"next_automatic_backup": None,
"next_automatic_backup_additional": False,
"state": "idle",
}

View File

@ -8,14 +8,14 @@
"country": "UnitedKingdom",
"postcode": "E1 1AA",
"locationType": "Residential",
"useDaylightSaveSwitching": true,
"timeZone": {
"timeZoneId": "GMTStandardTime",
"displayName": "(UTC+00:00) Dublin, Edinburgh, Lisbon, London",
"offsetMinutes": 0,
"currentOffsetMinutes": 60,
"timeZoneId": "PacificSAStandardTime",
"displayName": "(UTC-04:00) Santiago",
"offsetMinutes": -240,
"currentOffsetMinutes": -180,
"supportsDaylightSaving": true
},
"useDaylightSaveSwitching": true,
"locationOwner": {
"userId": "2263181",
"username": "user_2263181@gmail.com",

View File

@ -168,10 +168,10 @@
'target_heat_temperature': 16.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,
@ -215,10 +215,10 @@
'target_heat_temperature': 17.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': False,
@ -257,19 +257,19 @@
'activeFaults': tuple(
dict({
'fault_type': 'TempZoneActuatorLowBattery',
'since': '2022-03-02T04:50:20+00:00',
'since': '2022-03-02T04:50:20-03:00',
}),
),
'setpoint_status': dict({
'setpoint_mode': 'TemporaryOverride',
'target_heat_temperature': 21.0,
'until': '2022-03-07T19:00:00+00:00',
'until': '2022-03-07T16:00:00-03:00',
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,
@ -313,10 +313,10 @@
'target_heat_temperature': 17.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,
@ -360,10 +360,10 @@
'target_heat_temperature': 17.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,
@ -407,10 +407,10 @@
'target_heat_temperature': 16.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,
@ -450,7 +450,7 @@
'activeFaults': tuple(
dict({
'fault_type': 'TempZoneActuatorCommunicationLost',
'since': '2022-03-02T15:56:01+00:00',
'since': '2022-03-02T15:56:01-03:00',
}),
),
'setpoint_status': dict({
@ -458,10 +458,10 @@
'target_heat_temperature': 17.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,

View File

@ -2,10 +2,10 @@
# name: test_set_operation_mode[botched]
list([
dict({
'until': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=datetime.timezone.utc),
'until': HAFakeDatetime(2024, 7, 10, 12, 30, tzinfo=datetime.timezone.utc),
}),
dict({
'until': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=datetime.timezone.utc),
'until': HAFakeDatetime(2024, 7, 10, 12, 30, tzinfo=datetime.timezone.utc),
}),
])
# ---
@ -39,9 +39,9 @@
),
'dhw_id': '3933910',
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 13, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 30, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_state': 'Off',
'this_sp_from': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_from': HAFakeDatetime(2024, 7, 10, 6, 30, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_state': 'On',
}),
'state_status': dict({

View File

@ -11,7 +11,7 @@ import pytest
from homeassistant.auth.models import RefreshToken
from homeassistant.components.hassio.handler import HassIO, HassioAPIError
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.setup import async_setup_component
@ -75,7 +75,6 @@ def hassio_stubs(
"homeassistant.components.hassio.issues.SupervisorIssues.setup",
),
):
hass.set_state(CoreState.starting)
hass.loop.run_until_complete(async_setup_component(hass, "hassio", {}))
return hass_api.call_args[0][1]

View File

@ -106,6 +106,7 @@
'model': 'HEOS Drive HS2',
'name': 'Test Player',
'network': 'wired',
'preferred_host': True,
'serial': '**REDACTED**',
'supported_version': True,
'version': '1.0.0',
@ -116,6 +117,7 @@
'model': 'HEOS Drive HS2',
'name': 'Test Player',
'network': 'wired',
'preferred_host': True,
'serial': '**REDACTED**',
'supported_version': True,
'version': '1.0.0',
@ -125,6 +127,7 @@
'model': 'Speaker',
'name': 'Test Player 2',
'network': 'wifi',
'preferred_host': False,
'serial': '**REDACTED**',
'supported_version': True,
'version': '1.0.0',
@ -137,6 +140,7 @@
'model': 'HEOS Drive HS2',
'name': 'Test Player',
'network': 'wired',
'preferred_host': True,
'serial': '**REDACTED**',
'supported_version': True,
'version': '1.0.0',

View File

@ -14,7 +14,12 @@ from pyheos import (
import pytest
from homeassistant.components.heos.const import DOMAIN
from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER, ConfigEntryState
from homeassistant.config_entries import (
SOURCE_IGNORE,
SOURCE_SSDP,
SOURCE_USER,
ConfigEntryState,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
@ -83,6 +88,35 @@ async def test_create_entry_when_host_valid(
assert controller.disconnect.call_count == 1
async def test_manual_setup_with_discovery_in_progress(
hass: HomeAssistant,
discovery_data: SsdpServiceInfo,
controller: MockHeos,
system: HeosSystem,
) -> None:
"""Test user can manually set up when discovery is in progress."""
# Single discovered, selects preferred host, shows confirm
controller.get_system_info.return_value = system
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "confirm_discovery"
# Setup manually
user_result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert user_result["type"] is FlowResultType.FORM
user_result = await hass.config_entries.flow.async_configure(
user_result["flow_id"], user_input={CONF_HOST: "127.0.0.1"}
)
assert user_result["type"] is FlowResultType.CREATE_ENTRY
# Discovery flow is removed
assert not hass.config_entries.flow.async_progress_by_handler(DOMAIN)
async def test_discovery(
hass: HomeAssistant,
discovery_data: SsdpServiceInfo,
@ -160,6 +194,22 @@ async def test_discovery_aborts_same_system(
assert config_entry.data[CONF_HOST] == "127.0.0.1"
async def test_discovery_ignored_aborts(
hass: HomeAssistant,
discovery_data: SsdpServiceInfo,
) -> None:
"""Test discovery aborts when ignored."""
MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, source=SOURCE_IGNORE).add_to_hass(
hass
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"
async def test_discovery_fails_to_connect_aborts(
hass: HomeAssistant, discovery_data: SsdpServiceInfo, controller: MockHeos
) -> None:

View File

@ -285,11 +285,11 @@ async def test_reconnected_new_entities_created(
players = controller.players.copy()
players[3] = player_factory(3, "Test Player 3", "HEOS Link")
controller.mock_set_players(players)
controller.load_players.return_value = PlayerUpdateResult([3], [], {})
update = PlayerUpdateResult([3], [], {})
# Simulate reconnection
await controller.dispatcher.wait_send(
SignalType.HEOS_EVENT, SignalHeosEvent.CONNECTED
SignalType.CONTROLLER_EVENT, const.EVENT_PLAYERS_CHANGED, update
)
await hass.async_block_till_done()

View File

@ -158,7 +158,6 @@ async def test_updates_from_connection_event(
state = hass.states.get("media_player.test_player")
assert state is not None
assert state.state == STATE_IDLE
assert controller.load_players.call_count == 1
# Disconnected
controller.load_players.reset_mock()
@ -170,11 +169,8 @@ async def test_updates_from_connection_event(
state = hass.states.get("media_player.test_player")
assert state is not None
assert state.state == STATE_UNAVAILABLE
assert controller.load_players.call_count == 0
# Connected handles refresh failure
controller.load_players.reset_mock()
controller.load_players.side_effect = CommandFailedError("", "Failure", 1)
# Reconnect and state updates
player.available = True
await controller.dispatcher.wait_send(
SignalType.HEOS_EVENT, SignalHeosEvent.CONNECTED
@ -183,38 +179,6 @@ async def test_updates_from_connection_event(
state = hass.states.get("media_player.test_player")
assert state is not None
assert state.state == STATE_IDLE
assert controller.load_players.call_count == 1
assert "Unable to refresh players" in caplog.text
async def test_updates_from_connection_event_new_player_ids(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
config_entry: MockConfigEntry,
controller: MockHeos,
change_data_mapped_ids: PlayerUpdateResult,
) -> None:
"""Test player ids changed after reconnection updates ids."""
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
# Assert current IDs
assert device_registry.async_get_device(identifiers={(DOMAIN, "1")})
assert entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "1")
# Send event which will result in updated IDs.
controller.load_players.return_value = change_data_mapped_ids
await controller.dispatcher.wait_send(
SignalType.HEOS_EVENT, SignalHeosEvent.CONNECTED
)
await hass.async_block_till_done()
# Assert updated IDs and previous don't exist
assert not device_registry.async_get_device(identifiers={(DOMAIN, "1")})
assert device_registry.async_get_device(identifiers={(DOMAIN, "101")})
assert not entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "1")
assert entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "101")
async def test_updates_from_sources_updated(

View File

@ -8,9 +8,8 @@ from unittest.mock import MagicMock, patch
from aiohomeconnect.const import OAUTH2_TOKEN
from aiohomeconnect.model import OptionKey, ProgramKey, SettingKey, StatusKey
from aiohomeconnect.model.error import HomeConnectError, UnauthorizedError
import aiohttp
import pytest
import requests_mock
import respx
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
@ -221,14 +220,12 @@ async def test_exception_handling(
@pytest.mark.parametrize("token_expiration_time", [12345])
@respx.mock
async def test_token_refresh_success(
hass: HomeAssistant,
platforms: list[Platform],
integration_setup: Callable[[MagicMock], Awaitable[bool]],
config_entry: MockConfigEntry,
aioclient_mock: AiohttpClientMocker,
requests_mock: requests_mock.Mocker,
setup_credentials: None,
client: MagicMock,
) -> None:
@ -236,7 +233,6 @@ async def test_token_refresh_success(
assert config_entry.data["token"]["access_token"] == FAKE_ACCESS_TOKEN
requests_mock.post(OAUTH2_TOKEN, json=SERVER_ACCESS_TOKEN)
aioclient_mock.post(
OAUTH2_TOKEN,
json=SERVER_ACCESS_TOKEN,
@ -280,6 +276,61 @@ async def test_token_refresh_success(
)
@pytest.mark.parametrize("token_expiration_time", [12345])
@pytest.mark.parametrize(
("aioclient_mock_args", "expected_config_entry_state"),
[
(
{
"status": 400,
"json": {"error": "invalid_grant"},
},
ConfigEntryState.SETUP_ERROR,
),
(
{
"status": 500,
},
ConfigEntryState.SETUP_RETRY,
),
(
{
"exc": aiohttp.ClientError,
},
ConfigEntryState.SETUP_RETRY,
),
],
)
async def test_token_refresh_error(
aioclient_mock_args: dict[str, Any],
expected_config_entry_state: ConfigEntryState,
hass: HomeAssistant,
platforms: list[Platform],
integration_setup: Callable[[MagicMock], Awaitable[bool]],
config_entry: MockConfigEntry,
aioclient_mock: AiohttpClientMocker,
setup_credentials: None,
client: MagicMock,
) -> None:
"""Test where token is expired and the refresh attempt fails."""
config_entry.data["token"]["access_token"] = FAKE_ACCESS_TOKEN
aioclient_mock.post(
OAUTH2_TOKEN,
**aioclient_mock_args,
)
assert config_entry.state == ConfigEntryState.NOT_LOADED
with patch(
"homeassistant.components.home_connect.HomeConnectClient", return_value=client
):
assert not await integration_setup(client)
await hass.async_block_till_done()
assert config_entry.state == expected_config_entry_state
@pytest.mark.parametrize(
("exception", "expected_state"),
[

View File

@ -1556,6 +1556,42 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure(
assert insecure_check["insecure"] == insecure_param
@pytest.mark.parametrize(
("mqtt_config_entry_data", "client_id"),
[
(
{
mqtt.CONF_BROKER: "mock-broker",
"client_id": "random01234random0124",
},
"random01234random0124",
),
(
{
mqtt.CONF_BROKER: "mock-broker",
},
None,
),
],
)
async def test_client_id_is_set(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
client_id: str | None,
) -> None:
"""Test setup defaults for tls."""
with patch(
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
) as async_client_mock:
await mqtt_mock_entry()
await hass.async_block_till_done()
assert async_client_mock.call_count == 1
call_params: dict[str, Any] = async_client_mock.call_args[1]
assert "client_id" in call_params
assert client_id is None or client_id == call_params["client_id"]
assert call_params["client_id"] is not None
@pytest.mark.parametrize(
"mqtt_config_entry_data",
[

View File

@ -432,6 +432,65 @@ async def test_brightness_only(
assert state.state == STATE_OFF
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
light.DOMAIN: {
"schema": "json",
"name": "test",
"state_topic": "test_light_rgb",
"command_topic": "test_light_rgb/set",
"supported_color_modes": ["color_temp"],
}
}
},
],
)
async def test_color_temp_only(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:
"""Test a light that only support color_temp as supported color mode."""
await mqtt_mock_entry()
state = hass.states.get("light.test")
assert state.state == STATE_UNKNOWN
assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == [
light.ColorMode.COLOR_TEMP
]
expected_features = (
light.LightEntityFeature.FLASH | light.LightEntityFeature.TRANSITION
)
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
assert state.attributes.get("rgb_color") is None
assert state.attributes.get("brightness") is None
assert state.attributes.get("color_temp_kelvin") is None
assert state.attributes.get("effect") is None
assert state.attributes.get("xy_color") is None
assert state.attributes.get("hs_color") is None
async_fire_mqtt_message(
hass,
"test_light_rgb",
'{"state":"ON", "color_mode": "color_temp", "color_temp": 250, "brightness": 50}',
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
assert state.attributes.get("rgb_color") == (255, 206, 166)
assert state.attributes.get("brightness") == 50
assert state.attributes.get("color_temp_kelvin") == 4000
assert state.attributes.get("effect") is None
assert state.attributes.get("xy_color") == (0.42, 0.365)
assert state.attributes.get("hs_color") == (26.812, 34.87)
async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"OFF"}')
state = hass.states.get("light.test")
assert state.state == STATE_OFF
@pytest.mark.parametrize(
"hass_config",
[

View File

@ -870,32 +870,6 @@ async def test_invalid_device_class(
assert "expected SensorDeviceClass or one of" in caplog.text
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"device_class": "energy",
"unit_of_measurement": "ppm",
}
}
}
],
)
async def test_invalid_unit_of_measurement(
mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture
) -> None:
"""Test device_class with invalid unit of measurement."""
assert await mqtt_mock_entry()
assert (
"The unit of measurement `ppm` is not valid together with device class `energy`"
in caplog.text
)
@pytest.mark.parametrize(
"hass_config",
[

View File

@ -109,8 +109,8 @@
'entity_picture_local': None,
'friendly_name': 'Test Group Player 1',
'group_members': list([
'media_player.my_super_test_player_2',
'media_player.test_player_1',
'media_player.my_super_test_player_2',
]),
'icon': 'mdi:speaker-multiple',
'is_volume_muted': False,

View File

@ -9,6 +9,7 @@ from pysmartthings.models import (
DeviceStatus,
LocationResponse,
SceneResponse,
Subscription,
)
import pytest
@ -78,6 +79,9 @@ def mock_smartthings() -> Generator[AsyncMock]:
client.get_locations.return_value = LocationResponse.from_json(
load_fixture("locations.json", DOMAIN)
).items
client.create_subscription.return_value = Subscription.from_json(
load_fixture("subscription.json", DOMAIN)
)
yield client
@ -100,6 +104,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
"iphone",
"da_wm_dw_000001",
"da_wm_wd_000001",
"da_wm_wd_000001_1",
"da_wm_wm_000001",
"da_wm_wm_000001_1",
"da_rvc_normal_000001",
@ -115,9 +120,15 @@ def mock_smartthings() -> Generator[AsyncMock]:
"sensibo_airconditioner_1",
"ecobee_sensor",
"ecobee_thermostat",
"ecobee_thermostat_offline",
"fake_fan",
"generic_fan_3_speed",
"heatit_ztrm3_thermostat",
"generic_ef00_v1",
"bosch_radiator_thermostat_ii",
"im_speaker_ai_0001",
"abl_light_b_001",
"tplink_p110",
]
)
def device_fixture(

View File

@ -0,0 +1,27 @@
{
"components": {
"main": {
"switchLevel": {
"levelRange": {
"value": null
},
"level": {
"value": null
}
},
"switch": {
"switch": {
"value": null
}
},
"colorTemperature": {
"colorTemperatureRange": {
"value": null
},
"colorTemperature": {
"value": null
}
}
}
}
}

View File

@ -0,0 +1,89 @@
{
"components": {
"main": {
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 23.9,
"unit": "C",
"timestamp": "2025-03-07T19:55:13.328Z"
}
},
"thermostatHeatingSetpoint": {
"heatingSetpoint": {
"value": 22.0,
"unit": "C",
"timestamp": "2025-03-05T03:05:26.510Z"
},
"heatingSetpointRange": {
"value": {
"minimum": 5.0,
"maximum": 40.0,
"step": 0.1
},
"unit": "C",
"timestamp": "2025-03-05T03:05:26.510Z"
}
},
"refresh": {},
"thermostatMode": {
"thermostatMode": {
"value": "heat",
"data": {
"supportedThermostatModes": ["off", "heat"]
},
"timestamp": "2025-03-05T03:05:26.489Z"
},
"supportedThermostatModes": {
"value": ["off", "heat"],
"timestamp": "2025-03-05T03:05:26.509Z"
}
},
"battery": {
"quantity": {
"value": null
},
"battery": {
"value": 94,
"unit": "%",
"timestamp": "2025-03-07T20:47:27.362Z"
},
"type": {
"value": null
}
},
"firmwareUpdate": {
"lastUpdateStatusReason": {
"value": null
},
"availableVersion": {
"value": "2.00.09 (20009)",
"timestamp": "2024-11-29T19:55:02.005Z"
},
"lastUpdateStatus": {
"value": null
},
"supportedCommands": {
"value": null
},
"state": {
"value": "normalOperation",
"timestamp": "2024-11-29T19:55:02.009Z"
},
"updateAvailable": {
"value": false,
"timestamp": "2024-11-29T19:55:02.004Z"
},
"currentVersion": {
"value": "2.00.09 (20009)",
"timestamp": "2024-11-29T19:55:02.037Z"
},
"lastUpdateTime": {
"value": null
}
}
}
}
}

View File

@ -0,0 +1,692 @@
{
"components": {
"hca.main": {
"hca.dryerMode": {
"mode": {
"value": "normal",
"timestamp": "2025-03-09T16:31:41.247Z"
},
"supportedModes": {
"value": ["normal", "quickDry", "mix", "timeDry"],
"timestamp": "2025-03-09T16:31:40.486Z"
}
}
},
"main": {
"custom.dryerWrinklePrevent": {
"operatingState": {
"value": "ready",
"timestamp": "2025-03-09T16:31:40.486Z"
},
"dryerWrinklePrevent": {
"value": "off",
"timestamp": "2025-03-09T16:31:41.077Z"
}
},
"samsungce.dryerDryingTemperature": {
"dryingTemperature": {
"value": null,
"timestamp": "2021-04-02T18:31:36.756Z"
},
"supportedDryingTemperature": {
"value": null,
"timestamp": "2021-04-02T18:29:52.258Z"
}
},
"samsungce.welcomeMessage": {
"welcomeMessage": {
"value": null,
"timestamp": "2021-04-02T18:32:37.913Z"
}
},
"samsungce.dongleSoftwareInstallation": {
"status": {
"value": "completed",
"timestamp": "2022-06-17T17:07:35.734Z"
}
},
"samsungce.dryerCyclePreset": {
"maxNumberOfPresets": {
"value": 10,
"timestamp": "2025-03-09T16:31:41.229Z"
},
"presets": {
"value": null,
"timestamp": "2021-04-02T18:30:36.772Z"
}
},
"samsungce.deviceIdentification": {
"micomAssayCode": {
"value": "20221341",
"timestamp": "2025-03-09T16:31:40.834Z"
},
"modelName": {
"value": null,
"timestamp": "2021-04-02T18:29:53.622Z"
},
"serialNumber": {
"value": null,
"timestamp": "2021-04-02T18:29:52.641Z"
},
"serialNumberExtra": {
"value": null,
"timestamp": "2021-04-02T18:29:51.653Z"
},
"modelClassificationCode": {
"value": "30010102001211000103000000000000",
"timestamp": "2025-03-09T16:31:40.834Z"
},
"description": {
"value": "DA_WM_A51_20_COMMON_DV6800N/DC92-01967B_0404",
"timestamp": "2025-03-09T16:31:40.834Z"
},
"releaseYear": {
"value": null
},
"binaryId": {
"value": "DA_WM_A51_20_COMMON",
"timestamp": "2025-03-09T19:07:40.295Z"
}
},
"switch": {
"switch": {
"value": "off",
"timestamp": "2025-03-09T19:47:36.549Z"
}
},
"samsungce.quickControl": {
"version": {
"value": null
}
},
"samsungce.dryerFreezePrevent": {
"operatingState": {
"value": null
}
},
"ocf": {
"st": {
"value": null,
"timestamp": "2020-06-20T10:01:02.741Z"
},
"mndt": {
"value": null,
"timestamp": "2020-06-25T01:53:25.278Z"
},
"mnfv": {
"value": "DA_WM_A51_20_COMMON_30230708",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnhw": {
"value": "ARTIK051",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"di": {
"value": "3a6c4e05-811d-5041-e956-3d04c424cbcd",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnsl": {
"value": "http://www.samsung.com",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"dmv": {
"value": "res.1.1.0,sh.1.1.0",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"n": {
"value": "[dryer] Samsung",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnmo": {
"value": "DA_WM_A51_20_COMMON|20221341|30010102001211000103000000000000",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"vid": {
"value": "DA-WM-WD-000001",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnmn": {
"value": "Samsung Electronics",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnml": {
"value": "http://www.samsung.com",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnpv": {
"value": "DAWIT 2.0",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnos": {
"value": "TizenRT 1.0 + IPv6",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"pi": {
"value": "3a6c4e05-811d-5041-e956-3d04c424cbcd",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"icv": {
"value": "core.1.1.0",
"timestamp": "2024-12-15T10:53:49.561Z"
}
},
"custom.dryerDryLevel": {
"dryerDryLevel": {
"value": "2",
"timestamp": "2025-03-09T19:47:36.806Z"
},
"supportedDryerDryLevel": {
"value": ["none", "1", "2", "3"],
"timestamp": "2020-11-18T20:16:43.428Z"
}
},
"samsungce.dryerAutoCycleLink": {
"dryerAutoCycleLink": {
"value": null,
"timestamp": "2020-08-11T12:41:38.646Z"
}
},
"samsungce.dryerCycle": {
"dryerCycle": {
"value": "Table_00_Course_9A",
"timestamp": "2025-03-09T16:31:41.247Z"
},
"supportedCycles": {
"value": [
{
"cycle": "9A",
"supportedOptions": {
"dryingLevel": {
"raw": "D20E",
"default": "2",
"options": ["1", "2", "3"]
}
}
},
{
"cycle": "CA",
"supportedOptions": {
"dryingLevel": {
"raw": "D10E",
"default": "1",
"options": ["1", "2", "3"]
}
}
},
{
"cycle": "DB",
"supportedOptions": {
"dryingLevel": {
"raw": "D204",
"default": "2",
"options": ["2"]
}
}
},
{
"cycle": "99",
"supportedOptions": {
"dryingLevel": {
"raw": "D20E",
"default": "2",
"options": ["1", "2", "3"]
}
}
},
{
"cycle": "93",
"supportedOptions": {
"dryingLevel": {
"raw": "D102",
"default": "1",
"options": ["1"]
}
}
},
{
"cycle": "B5",
"supportedOptions": {
"dryingLevel": {
"raw": "D102",
"default": "1",
"options": ["1"]
}
}
},
{
"cycle": "D7",
"supportedOptions": {
"dryingLevel": {
"raw": "D204",
"default": "2",
"options": ["2"]
}
}
},
{
"cycle": "A5",
"supportedOptions": {
"dryingLevel": {
"raw": "D204",
"default": "2",
"options": ["2"]
}
}
},
{
"cycle": "96",
"supportedOptions": {
"dryingLevel": {
"raw": "D000",
"default": "none",
"options": []
}
}
},
{
"cycle": "97",
"supportedOptions": {
"dryingLevel": {
"raw": "D000",
"default": "none",
"options": []
}
}
},
{
"cycle": "7F",
"supportedOptions": {
"dryingLevel": {
"raw": "D000",
"default": "none",
"options": []
}
}
},
{
"cycle": "98",
"supportedOptions": {
"dryingLevel": {
"raw": "D000",
"default": "none",
"options": []
}
}
},
{
"cycle": "EB",
"supportedOptions": {
"dryingLevel": {
"raw": "D204",
"default": "2",
"options": ["2"]
}
}
},
{
"cycle": "B6",
"supportedOptions": {
"dryingLevel": {
"raw": "D20E",
"default": "2",
"options": ["1", "2", "3"]
}
}
}
],
"timestamp": "2025-02-10T02:24:03.524Z"
},
"referenceTable": {
"value": {
"id": "Table_00"
},
"timestamp": "2025-03-09T16:31:41.247Z"
},
"specializedFunctionClassification": {
"value": 4,
"timestamp": "2025-03-09T16:31:40.486Z"
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [
"samsungce.dryerDelayEnd",
"dryerOperatingState",
"samsungce.dryerCyclePreset",
"samsungce.welcomeMessage",
"samsungce.dongleSoftwareInstallation",
"sec.wifiConfiguration",
"samsungce.quickControl",
"samsungce.deviceInfoPrivate",
"demandResponseLoadControl",
"samsungce.dryerFreezePrevent",
"samsungce.dryerDryingTemperature",
"sec.diagnosticsInformation"
],
"timestamp": "2024-07-02T14:42:38.334Z"
}
},
"samsungce.driverVersion": {
"versionNumber": {
"value": 24110101,
"timestamp": "2024-12-02T07:43:41.263Z"
}
},
"sec.diagnosticsInformation": {
"logType": {
"value": null
},
"endpoint": {
"value": null
},
"minVersion": {
"value": null
},
"signinPermission": {
"value": null
},
"setupId": {
"value": null
},
"protocolType": {
"value": null
},
"tsId": {
"value": null
},
"mnId": {
"value": null
},
"dumpType": {
"value": null
}
},
"samsungce.kidsLock": {
"lockState": {
"value": "unlocked",
"timestamp": "2025-03-09T16:31:40.882Z"
}
},
"demandResponseLoadControl": {
"drlcStatus": {
"value": null
}
},
"samsungce.detergentOrder": {
"alarmEnabled": {
"value": false,
"timestamp": "2025-03-09T16:31:40.486Z"
},
"orderThreshold": {
"value": 0,
"unit": "cc",
"timestamp": "2025-03-09T16:31:40.486Z"
}
},
"powerConsumptionReport": {
"powerConsumption": {
"value": {
"energy": 796400,
"deltaEnergy": 0,
"power": 0,
"powerEnergy": 0.0,
"persistedEnergy": 0,
"energySaved": 0,
"start": "2025-03-09T19:47:26Z",
"end": "2025-03-09T19:47:37Z"
},
"timestamp": "2025-03-09T19:47:37.283Z"
}
},
"dryerOperatingState": {
"completionTime": {
"value": "2025-03-09T22:55:37Z",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"machineState": {
"value": "stop",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"supportedMachineStates": {
"value": ["stop", "run", "pause"],
"timestamp": "2025-03-09T16:31:41.172Z"
},
"dryerJobState": {
"value": "none",
"timestamp": "2025-03-09T19:47:37.015Z"
}
},
"samsungce.detergentState": {
"remainingAmount": {
"value": 0,
"unit": "cc",
"timestamp": "2025-03-09T16:31:40.486Z"
},
"dosage": {
"value": 0,
"unit": "cc",
"timestamp": "2025-03-09T16:31:40.486Z"
},
"initialAmount": {
"value": 0,
"unit": "cc",
"timestamp": "2025-03-09T16:31:40.486Z"
},
"detergentType": {
"value": "none",
"timestamp": "2021-04-02T18:29:51.428Z"
}
},
"samsungce.dryerDelayEnd": {
"remainingTime": {
"value": 0,
"unit": "min",
"timestamp": "2025-03-09T16:31:41.172Z"
}
},
"refresh": {},
"custom.jobBeginningStatus": {
"jobBeginningStatus": {
"value": null,
"timestamp": "2020-06-25T01:53:34.974Z"
}
},
"execute": {
"data": {
"value": {
"payload": {
"rt": ["x.com.samsung.da.information"],
"if": ["oic.if.baseline", "oic.if.a"],
"x.com.samsung.da.modelNum": "DA_WM_A51_20_COMMON|20221341|30010102001211000103000000000000",
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON_DV6800N/DC92-01967B_0404",
"x.com.samsung.da.serialNum": "0T625AEN100200N",
"x.com.samsung.da.otnDUID": "SHCDM6YAPCCXC",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON|20221341|30010102001211000103000000000000",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02198A220728(E256)",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "17111305,19060420",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
"data": {
"href": "/information/vs/0"
},
"timestamp": "2023-08-07T00:06:05.984Z"
}
},
"sec.wifiConfiguration": {
"autoReconnection": {
"value": null
},
"minVersion": {
"value": null
},
"supportedWiFiFreq": {
"value": null
},
"supportedAuthType": {
"value": null
},
"protocolType": {
"value": null
}
},
"remoteControlStatus": {
"remoteControlEnabled": {
"value": "false",
"timestamp": "2025-03-09T16:31:41.180Z"
}
},
"custom.supportedOptions": {
"course": {
"value": null
},
"referenceTable": {
"value": {
"id": "Table_00"
},
"timestamp": "2025-03-09T16:31:41.247Z"
},
"supportedCourses": {
"value": [
"9A",
"CA",
"DB",
"99",
"93",
"B5",
"D7",
"A5",
"96",
"97",
"7F",
"98",
"EB",
"B6"
],
"timestamp": "2025-03-09T16:31:40.486Z"
}
},
"custom.energyType": {
"energyType": {
"value": "2.0",
"timestamp": "2022-06-17T17:07:35.734Z"
},
"energySavingSupport": {
"value": false,
"timestamp": "2022-06-17T17:07:35.734Z"
},
"drMaxDuration": {
"value": null
},
"energySavingLevel": {
"value": null
},
"energySavingInfo": {
"value": null
},
"supportedEnergySavingLevels": {
"value": null
},
"energySavingOperation": {
"value": null
},
"notificationTemplateID": {
"value": null
},
"energySavingOperationSupport": {
"value": null
}
},
"samsungce.dryerOperatingState": {
"operatingState": {
"value": "ready",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"supportedOperatingStates": {
"value": ["ready", "running", "paused"],
"timestamp": "2022-11-01T12:48:22.390Z"
},
"scheduledJobs": {
"value": [
{
"jobName": "drying",
"timeInMin": 192
},
{
"jobName": "cooling",
"timeInMin": 1
}
],
"timestamp": "2025-03-09T16:31:40.486Z"
},
"progress": {
"value": 1,
"unit": "%",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"remainingTimeStr": {
"value": "03:08",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"dryerJobState": {
"value": "none",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"remainingTime": {
"value": 188,
"unit": "min",
"timestamp": "2025-03-09T19:47:37.015Z"
}
},
"samsungce.softwareUpdate": {
"targetModule": {
"value": null
},
"otnDUID": {
"value": "SHCDM6YAPCCXC",
"timestamp": "2025-03-09T16:31:40.834Z"
},
"lastUpdatedDate": {
"value": null
},
"availableModules": {
"value": [],
"timestamp": "2024-12-01T21:16:50.598Z"
},
"newVersionAvailable": {
"value": false,
"timestamp": "2024-12-01T21:16:50.598Z"
},
"operatingState": {
"value": null
},
"progress": {
"value": null
}
},
"samsungce.dryerDryingTime": {
"supportedDryingTime": {
"value": ["0", "30", "60", "90", "120", "150"],
"timestamp": "2021-04-02T18:29:51.428Z"
},
"dryingTime": {
"value": "0",
"unit": "min",
"timestamp": "2025-03-09T16:31:41.077Z"
}
}
}
}
}

View File

@ -0,0 +1,81 @@
{
"components": {
"main": {
"relativeHumidityMeasurement": {
"humidity": {
"value": null
}
},
"thermostatOperatingState": {
"thermostatOperatingState": {
"value": null
}
},
"healthCheck": {
"checkInterval": {
"value": 60,
"unit": "s",
"data": {
"deviceScheme": "UNTRACKED",
"protocol": "cloud"
},
"timestamp": "2025-03-10T00:57:26.866Z"
},
"healthStatus": {
"value": null
},
"DeviceWatch-Enroll": {
"value": null
},
"DeviceWatch-DeviceStatus": {
"value": "offline",
"data": {
"reason": "DEVICE-OFFLINE"
},
"timestamp": "2025-03-11T10:22:17.013Z"
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": null
}
},
"thermostatHeatingSetpoint": {
"heatingSetpoint": {
"value": null
},
"heatingSetpointRange": {
"value": null
}
},
"thermostatFanMode": {
"thermostatFanMode": {
"value": null
},
"supportedThermostatFanModes": {
"value": null
}
},
"refresh": {},
"thermostatMode": {
"thermostatMode": {
"value": null
},
"supportedThermostatModes": {
"value": null
}
},
"thermostatCoolingSetpoint": {
"coolingSetpointRange": {
"value": null
},
"coolingSetpoint": {
"value": null
}
}
}
}
}

View File

@ -0,0 +1,76 @@
{
"components": {
"main02": {
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 200.0,
"unit": "C",
"timestamp": "2024-12-02T20:18:52.095Z"
}
}
},
"main": {
"thermostatOperatingState": {
"thermostatOperatingState": {
"value": null
}
},
"signalStrength": {
"rssi": {
"value": -84,
"unit": "dBm",
"timestamp": "2025-03-07T20:53:55.346Z"
},
"lqi": {
"value": 255,
"timestamp": "2025-03-07T20:53:55.387Z"
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 21.0,
"unit": "C",
"timestamp": "2025-03-07T16:58:23.773Z"
}
},
"thermostatHeatingSetpoint": {
"heatingSetpoint": {
"value": 23.0,
"unit": "C",
"timestamp": "2025-02-10T17:48:38.299Z"
},
"heatingSetpointRange": {
"value": null
}
},
"refresh": {},
"valleyboard16460.debug": {
"value": {
"value": "<table style=\"font-size:0.6em;min-width:100%\"><tbody>\n <tr><th align=\"left\" style=\"width:35%\">Actual</th><td style=\"width:65%\">_TZE200_rxntag7i</td></tr>\n <tr><th align=\"left\">Expected</th><td>_TZE200_4hbx5cvx</td></tr>\n <tr><th align=\"left\">Profile</th><td>normal-thermostat-v3</td></tr>\n <tr><th align=\"left\">Mode</th><td>Similarity</td></tr>\n <tr><th align=\"left\">Preferences</th><td>Modified</td></tr>\n <tr><th align=\"left\">Exposes EF00</th><td>Yes</td></tr>\n <tr><th align=\"left\">Default DP</th><td>No</td></tr>\n </tbody></table>",
"timestamp": "2025-03-05T03:04:54.025Z"
}
},
"thermostatMode": {
"thermostatMode": {
"value": "heat",
"data": {},
"timestamp": "2024-12-30T08:22:19.273Z"
},
"supportedThermostatModes": {
"value": null
}
},
"switch": {
"switch": {
"value": null
}
}
}
}
}

View File

@ -0,0 +1,222 @@
{
"components": {
"main": {
"mediaPlayback": {
"supportedPlaybackCommands": {
"value": ["play", "pause", "stop"],
"timestamp": "2025-03-08T12:06:24.496Z"
},
"playbackStatus": {
"value": "stopped",
"timestamp": "2025-03-08T12:06:24.496Z"
}
},
"audioVolume": {
"volume": {
"value": 52,
"unit": "%",
"timestamp": "2025-03-08T12:08:00.153Z"
}
},
"mediaInputSource": {
"supportedInputSources": {
"value": null
},
"inputSource": {
"value": null
}
},
"audioTrackAddressing": {},
"samsungim.networkAudioGroupInfo": {
"groupName": {
"value": "",
"timestamp": "2025-03-08T12:06:24.628Z"
},
"role": {
"value": "",
"timestamp": "2025-03-08T12:06:24.628Z"
},
"channel": {
"value": "",
"timestamp": "2025-03-08T12:06:24.628Z"
},
"stereoType": {
"value": "A",
"timestamp": "2025-03-08T12:06:24.628Z"
},
"masterDi": {
"value": "",
"timestamp": "2025-03-08T12:06:24.628Z"
},
"acmMode": {
"value": "",
"timestamp": "2025-03-08T12:06:24.628Z"
},
"status": {
"value": "single",
"timestamp": "2025-03-08T12:06:24.628Z"
},
"masterName": {
"value": "",
"timestamp": "2025-03-08T12:06:24.628Z"
}
},
"refresh": {},
"audioNotification": {},
"execute": {
"data": {
"value": null
}
},
"samsungim.networkAudioMode": {
"mode": {
"value": "wifi",
"timestamp": "2025-03-08T12:06:24.573Z"
}
},
"mediaPlaybackRepeat": {
"playbackRepeatMode": {
"value": "off",
"timestamp": "2025-03-08T12:06:24.519Z"
}
},
"musicPlayer": {
"trackDescription": {
"value": null
},
"level": {
"value": null
},
"mute": {
"value": null
},
"trackData": {
"value": null
},
"status": {
"value": null
}
},
"ocf": {
"st": {
"value": null
},
"mndt": {
"value": null
},
"mnfv": {
"value": "V310XXU1AWK1",
"timestamp": "2025-03-08T12:06:18.931Z"
},
"mnhw": {
"value": "1.0",
"timestamp": "2025-03-08T12:06:18.931Z"
},
"di": {
"value": "c9276e43-fe3c-88c3-1dcc-2eb79e292b8c",
"timestamp": "2025-03-08T12:06:18.931Z"
},
"mnsl": {
"value": null
},
"dmv": {
"value": "IoTivity1.2.1",
"timestamp": "2025-03-08T12:06:18.942Z"
},
"n": {
"value": "Galaxy Home Mini (MQVL)",
"timestamp": "2025-03-08T12:06:18.931Z"
},
"mnmo": {
"value": "SM-V310",
"timestamp": "2025-03-08T12:06:18.931Z"
},
"vid": {
"value": "IM-SPEAKER-AI-0001",
"timestamp": "2025-03-08T12:06:18.931Z"
},
"mnmn": {
"value": "Samsung Electronics",
"timestamp": "2025-03-08T12:06:18.931Z"
},
"mnml": {
"value": null
},
"mnpv": {
"value": "4.0.0.1",
"timestamp": "2025-03-08T12:06:18.931Z"
},
"mnos": {
"value": "Tizen",
"timestamp": "2025-03-08T12:06:18.931Z"
},
"pi": {
"value": "c9276e43-fe3c-88c3-1dcc-2eb79e292b8c",
"timestamp": "2025-03-08T12:06:18.931Z"
},
"icv": {
"value": "core0.0.1",
"timestamp": "2025-03-08T12:06:18.942Z"
}
},
"samsungim.announcement": {
"enableState": {
"value": null
},
"supportedCategories": {
"value": null
},
"supportedTypes": {
"value": null
},
"supportedMimes": {
"value": null
}
},
"samsungim.bixbyContent": {
"supportedModes": {
"value": ["news", "weather", "music", "search_all"],
"timestamp": "2025-03-08T12:06:24.817Z"
}
},
"mediaPlaybackShuffle": {
"playbackShuffle": {
"value": "disabled",
"timestamp": "2025-03-08T12:06:24.592Z"
}
},
"audioMute": {
"mute": {
"value": "unmuted",
"timestamp": "2025-03-08T12:06:24.478Z"
}
},
"mediaTrackControl": {
"supportedTrackControlCommands": {
"value": null
}
},
"speechSynthesis": {},
"samsungim.networkAudioTrackData": {
"appName": {
"value": null
},
"source": {
"value": "wifi",
"timestamp": "2025-03-08T12:06:24.540Z"
}
},
"audioTrackData": {
"totalTime": {
"value": null
},
"audioTrackData": {
"value": null
},
"elapsedTime": {
"value": null
}
}
}
}
}

View File

@ -0,0 +1,46 @@
{
"components": {
"main": {
"powerConsumptionReport": {
"powerConsumption": {
"value": {
"start": "2025-03-10T14:43:42.500Z",
"end": "2025-03-10T14:59:42.500Z",
"energy": 15720,
"deltaEnergy": 0
},
"timestamp": "2025-03-10T14:59:50.010Z"
}
},
"healthCheck": {
"checkInterval": {
"value": 60,
"unit": "s",
"data": {
"deviceScheme": "UNTRACKED",
"protocol": "cloud"
},
"timestamp": "2024-03-07T21:14:59.839Z"
},
"healthStatus": {
"value": null
},
"DeviceWatch-Enroll": {
"value": null
},
"DeviceWatch-DeviceStatus": {
"value": "online",
"data": {},
"timestamp": "2025-03-10T14:14:37.232Z"
}
},
"refresh": {},
"switch": {
"switch": {
"value": "on",
"timestamp": "2025-03-10T14:14:37.232Z"
}
}
}
}
}

View File

@ -0,0 +1,59 @@
{
"items": [
{
"deviceId": "7c16163e-c94e-482f-95f6-139ae0cd9d5e",
"name": "ABL Wafer Down Light(BLE)",
"label": "Kitchen Light 5",
"manufacturerName": "Samsung Electronics",
"presentationId": "ABL-LIGHT-B-001",
"deviceManufacturerCode": "Samsung Electronics",
"locationId": "6c314222-8baf-48a0-9442-5b1102a8757f",
"ownerId": "f24ff388-700c-7d1e-91f2-1c37ae68ce2b",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "switch",
"version": 1
},
{
"id": "colorTemperature",
"version": 1
},
{
"id": "switchLevel",
"version": 1
}
],
"categories": [
{
"name": "Light",
"categoryType": "manufacturer"
}
]
}
],
"createTime": "2025-03-08T22:40:25.073Z",
"profile": {
"id": "65f5db53-9a78-4b19-8e40-d32187cd59ab"
},
"bleD2D": {
"encryptionKey": "f593369dcea915f6352a4a42cd4b2ea6",
"cipher": "AES_128-CBC-PKCS7Padding",
"advertisingId": "b13d7192",
"identifier": "88-57-1d-7c-cb-cf",
"configurationUrl": "https://apis.samsungiotcloud.com/v1/miniature/profile/65f5db53-9a78-4b19-8e40-d32187cd59ab",
"bleDeviceType": "BLE",
"metadata": null
},
"type": "BLE_D2D",
"restrictionTier": 0,
"allowed": null,
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}

View File

@ -0,0 +1,102 @@
{
"items": [
{
"deviceId": "286ba274-4093-4bcb-849c-a1a3efe7b1e5",
"name": "thermostat",
"label": "Radiator Thermostat II [+M] Wohnzimmer",
"manufacturerName": "SmartThingsCommunity",
"presentationId": "2a1c9915-f61b-3f3a-a02b-703b8cccf3d6",
"deviceManufacturerCode": "BOSCH",
"locationId": "0b6618a6-c3ab-4b6e-968d-59cc8c2761bc",
"ownerId": "8a20b799-9d87-ecdc-39de-c93c6e4d3ea1",
"roomId": "11374ab5-9b4e-416b-91d1-745bbf9b6db4",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "temperatureMeasurement",
"version": 1
},
{
"id": "thermostatMode",
"version": 1
},
{
"id": "thermostatHeatingSetpoint",
"version": 1
},
{
"id": "battery",
"version": 1
},
{
"id": "firmwareUpdate",
"version": 1
},
{
"id": "refresh",
"version": 1
}
],
"categories": [
{
"name": "Thermostat",
"categoryType": "manufacturer"
}
]
}
],
"createTime": "2024-11-29T19:55:00.910Z",
"parentDeviceId": "61bd280e-71c4-44fb-9b6e-53fdf14718a2",
"profile": {
"id": "4da5d086-111e-3084-a039-616974326833"
},
"matter": {
"driverId": "5f3c42eb-5704-4c95-9705-c51c1a6764bf",
"hubId": "61bd280e-71c4-44fb-9b6e-53fdf14718a2",
"provisioningState": "PROVISIONED",
"networkId": "8EF2CF7A212285B2-46C6B9F266A4521A",
"executingLocally": true,
"uniqueId": "8475B3FEFF6748D4",
"vendorId": 4617,
"productId": 12306,
"serialNumber": "D44867FFFEB37584",
"listeningType": "SLEEPY",
"supportedNetworkInterfaces": ["THREAD"],
"version": {
"hardware": 18,
"hardwareLabel": "1.2.0",
"software": 20009,
"softwareLabel": "2.00.09"
},
"endpoints": [
{
"endpointId": 0,
"deviceTypes": [
{
"deviceTypeId": 22
}
]
},
{
"endpointId": 1,
"deviceTypes": [
{
"deviceTypeId": 769
}
]
}
],
"syncDrivers": true
},
"type": "MATTER",
"restrictionTier": 0,
"allowed": null,
"executionContext": "LOCAL",
"relationships": []
}
],
"_links": {}
}

View File

@ -0,0 +1,205 @@
{
"items": [
{
"deviceId": "3a6c4e05-811d-5041-e956-3d04c424cbcd",
"name": "[dryer] Samsung",
"label": "Seca-Roupa",
"manufacturerName": "Samsung Electronics",
"presentationId": "DA-WM-WD-000001",
"deviceManufacturerCode": "Samsung Electronics",
"locationId": "06efa178-ad2f-4d22-838c-d63e05e5a58a",
"ownerId": "1a5f5619-e9ec-4302-beb9-633bb1657897",
"roomId": "dde24053-9707-49a5-ba0e-f19681514f37",
"deviceTypeName": "Samsung OCF Dryer",
"components": [
{
"id": "main",
"label": "Seca-Roupa",
"capabilities": [
{
"id": "ocf",
"version": 1
},
{
"id": "execute",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "switch",
"version": 1
},
{
"id": "remoteControlStatus",
"version": 1
},
{
"id": "dryerOperatingState",
"version": 1
},
{
"id": "powerConsumptionReport",
"version": 1
},
{
"id": "demandResponseLoadControl",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
},
{
"id": "custom.dryerDryLevel",
"version": 1
},
{
"id": "custom.dryerWrinklePrevent",
"version": 1
},
{
"id": "custom.energyType",
"version": 1
},
{
"id": "custom.jobBeginningStatus",
"version": 1
},
{
"id": "custom.supportedOptions",
"version": 1
},
{
"id": "samsungce.softwareUpdate",
"version": 1
},
{
"id": "samsungce.detergentOrder",
"version": 1
},
{
"id": "samsungce.detergentState",
"version": 1
},
{
"id": "samsungce.deviceIdentification",
"version": 1
},
{
"id": "samsungce.dongleSoftwareInstallation",
"version": 1
},
{
"id": "samsungce.driverVersion",
"version": 1
},
{
"id": "samsungce.dryerAutoCycleLink",
"version": 1
},
{
"id": "samsungce.dryerCycle",
"version": 1
},
{
"id": "samsungce.dryerCyclePreset",
"version": 1
},
{
"id": "samsungce.dryerDelayEnd",
"version": 1
},
{
"id": "samsungce.dryerDryingTemperature",
"version": 1
},
{
"id": "samsungce.dryerDryingTime",
"version": 1
},
{
"id": "samsungce.dryerFreezePrevent",
"version": 1
},
{
"id": "samsungce.dryerOperatingState",
"version": 1
},
{
"id": "samsungce.kidsLock",
"version": 1
},
{
"id": "samsungce.welcomeMessage",
"version": 1
},
{
"id": "samsungce.quickControl",
"version": 1
},
{
"id": "sec.diagnosticsInformation",
"version": 1
},
{
"id": "sec.wifiConfiguration",
"version": 1
}
],
"categories": [
{
"name": "Dryer",
"categoryType": "manufacturer"
}
]
},
{
"id": "hca.main",
"label": "hca.main",
"capabilities": [
{
"id": "hca.dryerMode",
"version": 1
}
],
"categories": [
{
"name": "Other",
"categoryType": "manufacturer"
}
]
}
],
"createTime": "2020-06-20T10:00:42Z",
"profile": {
"id": "53a1d049-eeda-396c-8324-e33438ef57be"
},
"ocf": {
"ocfDeviceType": "oic.d.dryer",
"name": "[dryer] Samsung",
"specVersion": "core.1.1.0",
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
"manufacturerName": "Samsung Electronics",
"modelNumber": "DA_WM_A51_20_COMMON|20221341|30010102001211000103000000000000",
"platformVersion": "DAWIT 2.0",
"platformOS": "TizenRT 1.0 + IPv6",
"hwVersion": "ARTIK051",
"firmwareVersion": "DA_WM_A51_20_COMMON_30230708",
"vendorId": "DA-WM-WD-000001",
"vendorResourceClientServerVersion": "ARTIK051 Release 2.210224.1",
"lastSignupTime": "2020-11-19T04:43:50.736Z",
"transferCandidate": false,
"additionalAuthCodeRequired": false
},
"type": "OCF",
"restrictionTier": 0,
"allowed": null,
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}

View File

@ -0,0 +1,82 @@
{
"items": [
{
"deviceId": "1888b38f-6246-4f1e-911b-bfcfb66999db",
"name": "v4 - ecobee Thermostat - Heat and Cool (F)",
"label": "Downstairs",
"manufacturerName": "0A0b",
"presentationId": "ST_5334da38-8076-4b40-9f6c-ac3fccaa5d24",
"deviceManufacturerCode": "ecobee",
"locationId": "1030449a-22c1-4a80-9781-0bd4ab7f0f2f",
"ownerId": "e7dbb793-4351-4cdc-b037-e6e0b4f9df67",
"roomId": "d22e6f98-78fe-4a76-b904-6cad8628da59",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "temperatureMeasurement",
"version": 1
},
{
"id": "relativeHumidityMeasurement",
"version": 1
},
{
"id": "thermostatHeatingSetpoint",
"version": 1
},
{
"id": "thermostatCoolingSetpoint",
"version": 1
},
{
"id": "thermostatOperatingState",
"version": 1
},
{
"id": "thermostatMode",
"version": 1
},
{
"id": "thermostatFanMode",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "healthCheck",
"version": 1
}
],
"categories": [
{
"name": "Thermostat",
"categoryType": "manufacturer"
}
]
}
],
"createTime": "2025-03-10T00:57:26.760Z",
"profile": {
"id": "234d537d-d388-497f-b0f4-2e25025119ba"
},
"viper": {
"manufacturerName": "ecobee",
"modelName": "nikeSmart-thermostat",
"swVersion": "250308073247",
"hwVersion": "250308073247",
"endpointAppId": "viper_92ccdcc0-4184-11eb-b9c5-036180216747"
},
"type": "VIPER",
"restrictionTier": 0,
"allowed": null,
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}

View File

@ -0,0 +1,95 @@
{
"items": [
{
"deviceId": "656569c2-7976-4232-a789-34b4d1176c3a",
"name": "generic-ef00-v1",
"label": "Thermostat K\u00fcche",
"manufacturerName": "SmartThingsCommunity",
"presentationId": "be641577-f796-315b-af6f-b3ad14dd7a58",
"deviceManufacturerCode": "_TZE200_rxntag7i",
"locationId": "0b6618a6-c3ab-4b6e-968d-59cc8c2761bc",
"ownerId": "8a20b799-9d87-ecdc-39de-c93c6e4d3ea1",
"roomId": "eeb2f9d2-19cc-4dad-9f23-28ec807de97e",
"components": [
{
"id": "main",
"label": "Thermostat",
"capabilities": [
{
"id": "switch",
"version": 1
},
{
"id": "thermostatMode",
"version": 1
},
{
"id": "temperatureMeasurement",
"version": 1
},
{
"id": "thermostatHeatingSetpoint",
"version": 1
},
{
"id": "thermostatOperatingState",
"version": 1
},
{
"id": "signalStrength",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "valleyboard16460.debug",
"version": 1
}
],
"categories": [
{
"name": "Thermostat",
"categoryType": "manufacturer"
}
]
},
{
"id": "main02",
"label": "Floor",
"capabilities": [
{
"id": "temperatureMeasurement",
"version": 1
}
],
"categories": [
{
"name": "Thermostat",
"categoryType": "manufacturer"
}
]
}
],
"createTime": "2024-12-02T15:58:01.598Z",
"profile": {
"id": "3ad2e1e3-8867-332c-85b5-b291602c324f"
},
"zigbee": {
"eui": "A4C1388B31017B5F",
"networkId": "162F",
"driverId": "585328e6-ac85-4ac5-bce4-286efd0ab980",
"executingLocally": true,
"hubId": "61bd280e-71c4-44fb-9b6e-53fdf14718a2",
"provisioningState": "DRIVER_SWITCH"
},
"type": "ZIGBEE",
"restrictionTier": 0,
"allowed": null,
"executionContext": "LOCAL",
"relationships": []
}
],
"_links": {}
}

View File

@ -0,0 +1,136 @@
{
"items": [
{
"deviceId": "c9276e43-fe3c-88c3-1dcc-2eb79e292b8c",
"name": "Galaxy Home Mini (MQVL)",
"label": "Galaxy Home Mini",
"manufacturerName": "Samsung Electronics",
"presentationId": "IM-SPEAKER-AI-0001",
"deviceManufacturerCode": "Samsung Electronics",
"locationId": "33db9e71-abe9-43a0-acd3-3f0927bbe5b7",
"ownerId": "9a1ee192-04ba-46ca-9c3d-196d8dbcf807",
"roomId": "445c006d-1796-4dd6-8308-1c3cd045e8ff",
"deviceTypeName": "Samsung OCF Network Audio Player",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "execute",
"version": 1
},
{
"id": "ocf",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "audioMute",
"version": 1
},
{
"id": "audioVolume",
"version": 1
},
{
"id": "mediaInputSource",
"version": 1
},
{
"id": "mediaPlaybackRepeat",
"version": 1
},
{
"id": "mediaPlaybackShuffle",
"version": 1
},
{
"id": "mediaPlayback",
"version": 1
},
{
"id": "mediaTrackControl",
"version": 1
},
{
"id": "audioTrackAddressing",
"version": 1
},
{
"id": "audioTrackData",
"version": 1
},
{
"id": "musicPlayer",
"version": 1
},
{
"id": "audioNotification",
"version": 1
},
{
"id": "speechSynthesis",
"version": 1
},
{
"id": "samsungim.bixbyContent",
"version": 1
},
{
"id": "samsungim.announcement",
"version": 1
},
{
"id": "samsungim.networkAudioMode",
"version": 1
},
{
"id": "samsungim.networkAudioGroupInfo",
"version": 1
},
{
"id": "samsungim.networkAudioTrackData",
"version": 1
}
],
"categories": [
{
"name": "NetworkAudio",
"categoryType": "manufacturer"
}
]
}
],
"createTime": "2025-03-08T12:06:18.865Z",
"profile": {
"id": "09df8e36-e94f-339c-9086-9639505e1fb2"
},
"ocf": {
"ocfDeviceType": "oic.d.networkaudio",
"name": "Galaxy Home Mini (MQVL)",
"specVersion": "core0.0.1",
"verticalDomainSpecVersion": "IoTivity1.2.1",
"manufacturerName": "Samsung Electronics",
"modelNumber": "SM-V310",
"platformVersion": "4.0.0.1",
"platformOS": "Tizen",
"hwVersion": "1.0",
"firmwareVersion": "V310XXU1AWK1",
"vendorId": "IM-SPEAKER-AI-0001",
"lastSignupTime": "2025-03-08T12:06:16.386696652Z",
"transferCandidate": false,
"additionalAuthCodeRequired": false
},
"type": "OCF",
"restrictionTier": 0,
"allowed": null,
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}

View File

@ -0,0 +1,73 @@
{
"items": [
{
"deviceId": "6602696a-1e48-49e4-919f-69406f5b5da1",
"name": "plug-energy-usage-report",
"label": "Sp\u00fclmaschine",
"manufacturerName": "0AI2",
"presentationId": "ST_8f2be0ec-1113-46e0-ad56-3e92eb27410f",
"deviceManufacturerCode": "TP-Link",
"locationId": "70da36b0-bd25-410c-beed-7f0dbf658448",
"ownerId": "be5d4173-dd49-1eee-56f5-f98306ee872c",
"roomId": "bd13616d-b7e2-44ff-914c-eb38ea18c4b4",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "healthCheck",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "switch",
"version": 1
},
{
"id": "powerConsumptionReport",
"version": 1
}
],
"categories": [
{
"name": "SmartPlug",
"categoryType": "manufacturer"
},
{
"name": "SmartPlug",
"categoryType": "user"
}
]
}
],
"createTime": "2024-03-07T21:14:59.762Z",
"profile": {
"id": "a25b207e-cbb9-40ae-8a88-906637c22ab6"
},
"viper": {
"uniqueIdentifier": "8022F7F6FE0A6EACA52B5D89C0D667352136D8C6",
"manufacturerName": "TP-Link",
"modelName": "P110",
"swVersion": "1.3.1 Build 240621 Rel.162048",
"hwVersion": "1.0",
"endpointAppId": "viper_7ea6bb80-b876-11eb-be42-952f31ab3f7b"
},
"type": "VIPER",
"restrictionTier": 0,
"allowed": null,
"indoorMap": {
"coordinates": [0.0, 0.0, 0.0],
"rotation": [0.0, 180.0, 0.0],
"visible": false,
"data": null
},
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}

View File

@ -0,0 +1,16 @@
{
"subscriptionId": "f5768ce8-c9e5-4507-9020-912c0c60e0ab",
"registrationUrl": "https://spigot-regional.api.smartthings.com/filters/f5768ce8-c9e5-4507-9020-912c0c60e0ab/activate?filterRegion=eu-west-1",
"name": "My Home Assistant sub",
"version": 20250122,
"subscriptionFilters": [
{
"type": "LOCATIONIDS",
"value": ["88a3a314-f0c8-40b4-bb44-44ba06c9c42e"],
"eventType": ["DEVICE_EVENT"],
"attribute": null,
"capability": null,
"component": null
}
]
}

View File

@ -1,4 +1,67 @@
# serializer version: 1
# name: test_all_entities[bosch_radiator_thermostat_ii][climate.radiator_thermostat_ii_m_wohnzimmer-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 35,
'min_temp': 7,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.radiator_thermostat_ii_m_wohnzimmer',
'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': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 387>,
'translation_key': None,
'unique_id': '286ba274-4093-4bcb-849c-a1a3efe7b1e5',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[bosch_radiator_thermostat_ii][climate.radiator_thermostat_ii_m_wohnzimmer-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 23.9,
'friendly_name': 'Radiator Thermostat II [+M] Wohnzimmer',
'hvac_modes': list([
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 35,
'min_temp': 7,
'supported_features': <ClimateEntityFeature: 387>,
'target_temp_high': None,
'target_temp_low': None,
'temperature': 22.0,
}),
'context': <ANY>,
'entity_id': 'climate.radiator_thermostat_ii_m_wohnzimmer',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'heat',
})
# ---
# name: test_all_entities[da_ac_rac_000001][climate.ac_office_granit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -369,6 +432,131 @@
'state': 'heat',
})
# ---
# name: test_all_entities[ecobee_thermostat_offline][climate.downstairs-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'fan_modes': None,
'hvac_modes': list([
]),
'max_temp': 35,
'min_temp': 7,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.downstairs',
'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': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 395>,
'translation_key': None,
'unique_id': '1888b38f-6246-4f1e-911b-bfcfb66999db',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[ecobee_thermostat_offline][climate.downstairs-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': None,
'fan_mode': None,
'fan_modes': None,
'friendly_name': 'Downstairs',
'hvac_modes': list([
]),
'max_temp': 35,
'min_temp': 7,
'supported_features': <ClimateEntityFeature: 395>,
'target_temp_high': None,
'target_temp_low': None,
'temperature': None,
}),
'context': <ANY>,
'entity_id': 'climate.downstairs',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_entities[generic_ef00_v1][climate.thermostat_kuche-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
]),
'max_temp': 35,
'min_temp': 7,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.thermostat_kuche',
'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': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 387>,
'translation_key': None,
'unique_id': '656569c2-7976-4232-a789-34b4d1176c3a',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[generic_ef00_v1][climate.thermostat_kuche-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 21.0,
'friendly_name': 'Thermostat Küche',
'hvac_modes': list([
]),
'max_temp': 35,
'min_temp': 7,
'supported_features': <ClimateEntityFeature: 387>,
'target_temp_high': None,
'target_temp_low': None,
'temperature': 23.0,
}),
'context': <ANY>,
'entity_id': 'climate.thermostat_kuche',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'heat',
})
# ---
# name: test_all_entities[heatit_ztrm3_thermostat][climate.hall_thermostat-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -2,6 +2,39 @@
# name: test_button_event[button]
<Event smartthings.button[L]: component_id=main, device_id=c4bdd19f-85d1-4d58-8f9c-e75ac3cf113b, location_id=abc, value=pushed, name=button, data=None>
# ---
# name: test_devices[abl_light_b_001]
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': None,
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'7c16163e-c94e-482f-95f6-139ae0cd9d5e',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'model_id': None,
'name': 'Kitchen Light 5',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_devices[aeotec_home_energy_meter_gen5]
DeviceRegistryEntrySnapshot({
'area_id': None,
@ -68,6 +101,39 @@
'via_device_id': None,
})
# ---
# name: test_devices[bosch_radiator_thermostat_ii]
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': None,
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'286ba274-4093-4bcb-849c-a1a3efe7b1e5',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'model_id': None,
'name': 'Radiator Thermostat II [+M] Wohnzimmer',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_devices[c2c_arlo_pro_3_switch]
DeviceRegistryEntrySnapshot({
'area_id': None,
@ -464,6 +530,39 @@
'via_device_id': None,
})
# ---
# name: test_devices[da_wm_wd_000001_1]
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': 'ARTIK051',
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'3a6c4e05-811d-5041-e956-3d04c424cbcd',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Samsung Electronics',
'model': 'DA_WM_A51_20_COMMON',
'model_id': None,
'name': 'Seca-Roupa',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': 'DA_WM_A51_20_COMMON_30230708',
'via_device_id': None,
})
# ---
# name: test_devices[da_wm_wm_000001]
DeviceRegistryEntrySnapshot({
'area_id': None,
@ -596,6 +695,39 @@
'via_device_id': None,
})
# ---
# name: test_devices[ecobee_thermostat_offline]
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': '250308073247',
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'1888b38f-6246-4f1e-911b-bfcfb66999db',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'ecobee',
'model': 'nikeSmart-thermostat',
'model_id': None,
'name': 'Downstairs',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '250308073247',
'via_device_id': None,
})
# ---
# name: test_devices[fake_fan]
DeviceRegistryEntrySnapshot({
'area_id': None,
@ -662,6 +794,39 @@
'via_device_id': None,
})
# ---
# name: test_devices[generic_ef00_v1]
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': None,
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'656569c2-7976-4232-a789-34b4d1176c3a',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'model_id': None,
'name': 'Thermostat Küche',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_devices[generic_fan_3_speed]
DeviceRegistryEntrySnapshot({
'area_id': None,
@ -794,6 +959,39 @@
'via_device_id': None,
})
# ---
# name: test_devices[im_speaker_ai_0001]
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': '1.0',
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'c9276e43-fe3c-88c3-1dcc-2eb79e292b8c',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Samsung Electronics',
'model': 'SM-V310',
'model_id': None,
'name': 'Galaxy Home Mini',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': 'V310XXU1AWK1',
'via_device_id': None,
})
# ---
# name: test_devices[iphone]
DeviceRegistryEntrySnapshot({
'area_id': None,
@ -959,6 +1157,39 @@
'via_device_id': None,
})
# ---
# name: test_devices[tplink_p110]
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': '1.0',
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'6602696a-1e48-49e4-919f-69406f5b5da1',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'TP-Link',
'model': 'P110',
'model_id': None,
'name': 'Spülmaschine',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '1.3.1 Build 240621 Rel.162048',
'via_device_id': None,
})
# ---
# name: test_devices[vd_network_audio_002s]
DeviceRegistryEntrySnapshot({
'area_id': None,

View File

@ -1,4 +1,74 @@
# serializer version: 1
# name: test_all_entities[abl_light_b_001][light.kitchen_light_5-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max_color_temp_kelvin': 9000,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 111,
'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.kitchen_light_5',
'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': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': <LightEntityFeature: 32>,
'translation_key': None,
'unique_id': '7c16163e-c94e-482f-95f6-139ae0cd9d5e',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[abl_light_b_001][light.kitchen_light_5-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'brightness': None,
'color_mode': None,
'color_temp': None,
'color_temp_kelvin': None,
'friendly_name': 'Kitchen Light 5',
'hs_color': None,
'max_color_temp_kelvin': 9000,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 111,
'rgb_color': None,
'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
]),
'supported_features': <LightEntityFeature: 32>,
'xy_color': None,
}),
'context': <ANY>,
'entity_id': 'light.kitchen_light_5',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_entities[centralite][light.dimmer_debian-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

File diff suppressed because it is too large Load Diff

View File

@ -234,6 +234,53 @@
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_wd_000001_1][switch.seca_roupa-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.seca_roupa',
'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': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_wd_000001_1][switch.seca_roupa-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Seca-Roupa',
}),
'context': <ANY>,
'entity_id': 'switch.seca_roupa',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_wm_000001][switch.washer-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -328,6 +375,53 @@
'state': 'on',
})
# ---
# name: test_all_entities[generic_ef00_v1][switch.thermostat_kuche-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.thermostat_kuche',
'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': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '656569c2-7976-4232-a789-34b4d1176c3a',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[generic_ef00_v1][switch.thermostat_kuche-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Thermostat Küche',
}),
'context': <ANY>,
'entity_id': 'switch.thermostat_kuche',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[sensibo_airconditioner_1][switch.office-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -422,6 +516,53 @@
'state': 'on',
})
# ---
# name: test_all_entities[tplink_p110][switch.spulmaschine-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.spulmaschine',
'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': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '6602696a-1e48-49e4-919f-69406f5b5da1',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[tplink_p110][switch.spulmaschine-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Spülmaschine',
}),
'context': <ANY>,
'entity_id': 'switch.spulmaschine',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[vd_network_audio_002s][switch.soundbar_living-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -10,6 +10,7 @@ from homeassistant.components.smartthings.const import (
CONF_INSTALLED_APP_ID,
CONF_LOCATION_ID,
CONF_REFRESH_TOKEN,
CONF_SUBSCRIPTION_ID,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
@ -508,6 +509,7 @@ async def test_migration(
"installed_app_id": "123123123-2be1-4e40-b257-e4ef59083324",
},
CONF_LOCATION_ID: "397678e5-9995-4a39-9d9f-ae6ba310236c",
CONF_SUBSCRIPTION_ID: "f5768ce8-c9e5-4507-9020-912c0c60e0ab",
}
assert mock_old_config_entry.unique_id == "397678e5-9995-4a39-9d9f-ae6ba310236c"
assert mock_old_config_entry.version == 3

View File

@ -2,18 +2,21 @@
from unittest.mock import AsyncMock
from pysmartthings import Attribute, Capability
from pysmartthings import Attribute, Capability, SmartThingsSinkError
from pysmartthings.models import Subscription
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.smartthings import EVENT_BUTTON
from homeassistant.components.smartthings.const import DOMAIN
from homeassistant.components.smartthings.const import CONF_SUBSCRIPTION_ID, DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration, trigger_update
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, load_fixture
async def test_devices(
@ -63,6 +66,178 @@ async def test_button_event(
assert events[0] == snapshot
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
async def test_create_subscription(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test creating a subscription."""
assert CONF_SUBSCRIPTION_ID not in mock_config_entry.data
await setup_integration(hass, mock_config_entry)
devices.create_subscription.assert_called_once()
assert (
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
)
devices.subscribe.assert_called_once_with(
"397678e5-9995-4a39-9d9f-ae6ba310236c",
"5aaaa925-2be1-4e40-b257-e4ef59083324",
Subscription.from_json(load_fixture("subscription.json", DOMAIN)),
)
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
async def test_create_subscription_sink_error(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test handling an error when creating a subscription."""
assert CONF_SUBSCRIPTION_ID not in mock_config_entry.data
devices.create_subscription.side_effect = SmartThingsSinkError("Sink error")
await setup_integration(hass, mock_config_entry)
devices.subscribe.assert_not_called()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
assert CONF_SUBSCRIPTION_ID not in mock_config_entry.data
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
async def test_update_subscription_identifier(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test updating the subscription identifier."""
await setup_integration(hass, mock_config_entry)
assert (
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
)
devices.new_subscription_id_callback("abc")
await hass.async_block_till_done()
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] == "abc"
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
async def test_stale_subscription_id(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test updating the subscription identifier."""
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
mock_config_entry,
data={**mock_config_entry.data, CONF_SUBSCRIPTION_ID: "test"},
)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert (
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
)
devices.delete_subscription.assert_called_once_with("test")
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
async def test_remove_subscription_identifier(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test removing the subscription identifier."""
await setup_integration(hass, mock_config_entry)
assert (
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
)
devices.new_subscription_id_callback(None)
await hass.async_block_till_done()
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] is None
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
async def test_max_connections_handling(
hass: HomeAssistant, devices: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test handling reaching max connections."""
await setup_integration(hass, mock_config_entry)
assert (
mock_config_entry.data[CONF_SUBSCRIPTION_ID]
== "f5768ce8-c9e5-4507-9020-912c0c60e0ab"
)
devices.create_subscription.side_effect = SmartThingsSinkError("Sink error")
devices.max_connections_reached_callback()
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
async def test_unloading(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test unloading the integration."""
await setup_integration(hass, mock_config_entry)
await hass.config_entries.async_unload(mock_config_entry.entry_id)
devices.delete_subscription.assert_called_once_with(
"f5768ce8-c9e5-4507-9020-912c0c60e0ab"
)
# Deleting the subscription automatically deletes the subscription ID
devices.new_subscription_id_callback(None)
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] is None
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
async def test_shutdown(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test shutting down Home Assistant."""
await setup_integration(hass, mock_config_entry)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
devices.delete_subscription.assert_called_once_with(
"f5768ce8-c9e5-4507-9020-912c0c60e0ab"
)
# Deleting the subscription automatically deletes the subscription ID
devices.new_subscription_id_callback(None)
assert mock_config_entry.state is ConfigEntryState.LOADED
assert mock_config_entry.data[CONF_SUBSCRIPTION_ID] is None
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
async def test_removing_stale_devices(
hass: HomeAssistant,

View File

@ -34,5 +34,23 @@
"protocol_info": "a:b:c:d"
}
]
},
{
"title": "American Tall Tales",
"parent_id": "FV:2",
"item_id": "FV:2/66",
"restricted": false,
"resource_meta_data": "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\"><item id=\"101340c8reftitleC9F27_com\" parentID=\"101340c8reftitleC9F27_com\" restricted=\"true\"><dc:title>American Tall Tales</dc:title><upnp:class>object.item.audioItem.audioBook</upnp:class><desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">SA_RINCON61191_X_#Svc6-0-Token</desc></item></DIDL-Lite>",
"resources": [
{
"uri": "x-rincon-cpcontainer:101340c8reftitle%C9F27_com?sid=239&flags=16584&sn=5",
"protocol_info": "x-rincon-cpcontainer:*:*:*"
}
],
"desc": null,
"album_art_uri": "https://m.media-amazon.com/images/I/810lqLo5m0L._SL600_.jpg",
"type": "instantPlay",
"description": "Audible",
"favorite_nr": "0"
}
]

View File

@ -1,4 +1,74 @@
# serializer version: 1
# name: test_browse_media_favorites[-favorites]
dict({
'can_expand': True,
'can_play': False,
'children': list([
dict({
'can_expand': True,
'can_play': False,
'children_media_class': None,
'media_class': 'album',
'media_content_id': 'object.container.album.musicAlbum',
'media_content_type': 'favorites_folder',
'thumbnail': None,
'title': 'Albums',
}),
dict({
'can_expand': True,
'can_play': False,
'children_media_class': None,
'media_class': 'track',
'media_content_id': 'object.item.audioItem.audioBook',
'media_content_type': 'favorites_folder',
'thumbnail': None,
'title': 'Audio Book',
}),
dict({
'can_expand': True,
'can_play': False,
'children_media_class': None,
'media_class': 'genre',
'media_content_id': 'object.item.audioItem.audioBroadcast',
'media_content_type': 'favorites_folder',
'thumbnail': None,
'title': 'Radio',
}),
]),
'children_media_class': 'directory',
'media_class': 'directory',
'media_content_id': '',
'media_content_type': 'favorites',
'not_shown': 0,
'thumbnail': None,
'title': 'Favorites',
})
# ---
# name: test_browse_media_favorites[object.item.audioItem.audioBook-favorites_folder]
dict({
'can_expand': True,
'can_play': False,
'children': list([
dict({
'can_expand': False,
'can_play': True,
'children_media_class': None,
'media_class': 'track',
'media_content_id': 'FV:2/66',
'media_content_type': 'favorite_item_id',
'thumbnail': 'https://m.media-amazon.com/images/I/810lqLo5m0L._SL600_.jpg',
'title': 'American Tall Tales',
}),
]),
'children_media_class': 'track',
'media_class': 'directory',
'media_content_id': '',
'media_content_type': 'favorites',
'not_shown': 0,
'thumbnail': None,
'title': 'Audio Book',
})
# ---
# name: test_browse_media_library
list([
dict({

View File

@ -2,6 +2,7 @@
from functools import partial
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType
@ -176,3 +177,39 @@ async def test_browse_media_library_albums(
assert response["success"]
assert response["result"]["children"] == snapshot
assert soco_mock.music_library.browse_by_idstring.call_count == 1
@pytest.mark.parametrize(
("media_content_id", "media_content_type"),
[
(
"",
"favorites",
),
(
"object.item.audioItem.audioBook",
"favorites_folder",
),
],
)
async def test_browse_media_favorites(
async_autosetup_sonos,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
media_content_id,
media_content_type,
) -> None:
"""Test the async_browse_media method."""
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "media_player/browse_media",
"entity_id": "media_player.zone_a",
"media_content_id": media_content_id,
"media_content_type": media_content_type,
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == snapshot

View File

@ -1 +1,14 @@
"""Tests for the WebDAV integration."""
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Set up the WebDAV integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -62,4 +62,5 @@ def mock_webdav_client() -> Generator[AsyncMock]:
mock.download_iter.side_effect = _download_mock
mock.upload_iter.return_value = None
mock.clean.return_value = None
mock.move.return_value = None
yield mock

View File

@ -0,0 +1,96 @@
"""Test WebDAV component setup."""
from unittest.mock import AsyncMock
from aiowebdav2.exceptions import WebDavError
import pytest
from homeassistant.components.webdav.const import CONF_BACKUP_PATH, DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from . import setup_integration
from tests.common import MockConfigEntry
async def test_migrate_wrong_path(
hass: HomeAssistant, webdav_client: AsyncMock
) -> None:
"""Test migration of wrong encoded folder path."""
webdav_client.list_with_properties.return_value = [
{"/wrong%20path": []},
]
config_entry = MockConfigEntry(
title="user@webdav.demo",
domain=DOMAIN,
data={
CONF_URL: "https://webdav.demo",
CONF_USERNAME: "user",
CONF_PASSWORD: "supersecretpassword",
CONF_BACKUP_PATH: "/wrong path",
},
entry_id="01JKXV07ASC62D620DGYNG2R8H",
)
await setup_integration(hass, config_entry)
webdav_client.move.assert_called_once_with("/wrong%20path", "/wrong path")
async def test_migrate_non_wrong_path(
hass: HomeAssistant, webdav_client: AsyncMock
) -> None:
"""Test no migration of correct folder path."""
webdav_client.list_with_properties.return_value = [
{"/correct path": []},
]
webdav_client.check.side_effect = lambda path: path == "/correct path"
config_entry = MockConfigEntry(
title="user@webdav.demo",
domain=DOMAIN,
data={
CONF_URL: "https://webdav.demo",
CONF_USERNAME: "user",
CONF_PASSWORD: "supersecretpassword",
CONF_BACKUP_PATH: "/correct path",
},
entry_id="01JKXV07ASC62D620DGYNG2R8H",
)
await setup_integration(hass, config_entry)
webdav_client.move.assert_not_called()
async def test_migrate_error(
hass: HomeAssistant,
webdav_client: AsyncMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test migration of wrong encoded folder path with error."""
webdav_client.list_with_properties.return_value = [
{"/wrong%20path": []},
]
webdav_client.move.side_effect = WebDavError("Failed to move")
config_entry = MockConfigEntry(
title="user@webdav.demo",
domain=DOMAIN,
data={
CONF_URL: "https://webdav.demo",
CONF_USERNAME: "user",
CONF_PASSWORD: "supersecretpassword",
CONF_BACKUP_PATH: "/wrong path",
},
entry_id="01JKXV07ASC62D620DGYNG2R8H",
)
await setup_integration(hass, config_entry)
assert config_entry.state is ConfigEntryState.SETUP_RETRY
assert (
'Failed to migrate wrong encoded folder "/wrong%20path" to "/wrong path"'
in caplog.text
)