diff --git a/homeassistant/components/comelit/binary_sensor.py b/homeassistant/components/comelit/binary_sensor.py index 68390642c877dd..915c20473c265a 100644 --- a/homeassistant/components/comelit/binary_sensor.py +++ b/homeassistant/components/comelit/binary_sensor.py @@ -15,6 +15,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .coordinator import ComelitConfigEntry, ComelitVedoSystem +from .utils import DeviceType, new_device_listener # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -29,23 +30,19 @@ async def async_setup_entry( coordinator = cast(ComelitVedoSystem, config_entry.runtime_data) - known_devices: set[int] = set() - - def _check_device() -> None: - current_devices = set(coordinator.data["alarm_zones"]) - new_devices = current_devices - known_devices - if new_devices: - known_devices.update(new_devices) - async_add_entities( - ComelitVedoBinarySensorEntity( - coordinator, device, config_entry.entry_id - ) - for device in coordinator.data["alarm_zones"].values() - if device.index in new_devices - ) - - _check_device() - config_entry.async_on_unload(coordinator.async_add_listener(_check_device)) + def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None: + """Add entities for new monitors.""" + entities = [ + ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id) + for device in coordinator.data["alarm_zones"].values() + if device in new_devices + ] + if entities: + async_add_entities(entities) + + config_entry.async_on_unload( + new_device_listener(coordinator, _add_new_entities, "alarm_zones") + ) class ComelitVedoBinarySensorEntity( diff --git a/homeassistant/components/comelit/cover.py b/homeassistant/components/comelit/cover.py index 1eff1449eb328a..b42c34c945d8de 100644 --- a/homeassistant/components/comelit/cover.py +++ b/homeassistant/components/comelit/cover.py @@ -21,7 +21,7 @@ from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .entity import ComelitBridgeBaseEntity -from .utils import bridge_api_call +from .utils import DeviceType, bridge_api_call, new_device_listener # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -36,21 +36,19 @@ async def async_setup_entry( coordinator = cast(ComelitSerialBridge, config_entry.runtime_data) - known_devices: set[int] = set() - - def _check_device() -> None: - current_devices = set(coordinator.data[COVER]) - new_devices = current_devices - known_devices - if new_devices: - known_devices.update(new_devices) - async_add_entities( - ComelitCoverEntity(coordinator, device, config_entry.entry_id) - for device in coordinator.data[COVER].values() - if device.index in new_devices - ) - - _check_device() - config_entry.async_on_unload(coordinator.async_add_listener(_check_device)) + def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None: + """Add entities for new monitors.""" + entities = [ + ComelitCoverEntity(coordinator, device, config_entry.entry_id) + for device in coordinator.data[dev_type].values() + if device in new_devices + ] + if entities: + async_add_entities(entities) + + config_entry.async_on_unload( + new_device_listener(coordinator, _add_new_entities, COVER) + ) class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity): diff --git a/homeassistant/components/comelit/light.py b/homeassistant/components/comelit/light.py index 8ff626ed9166b1..95414ab8d0f936 100644 --- a/homeassistant/components/comelit/light.py +++ b/homeassistant/components/comelit/light.py @@ -12,7 +12,7 @@ from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .entity import ComelitBridgeBaseEntity -from .utils import bridge_api_call +from .utils import DeviceType, bridge_api_call, new_device_listener # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -27,21 +27,19 @@ async def async_setup_entry( coordinator = cast(ComelitSerialBridge, config_entry.runtime_data) - known_devices: set[int] = set() - - def _check_device() -> None: - current_devices = set(coordinator.data[LIGHT]) - new_devices = current_devices - known_devices - if new_devices: - known_devices.update(new_devices) - async_add_entities( - ComelitLightEntity(coordinator, device, config_entry.entry_id) - for device in coordinator.data[LIGHT].values() - if device.index in new_devices - ) - - _check_device() - config_entry.async_on_unload(coordinator.async_add_listener(_check_device)) + def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None: + """Add entities for new monitors.""" + entities = [ + ComelitLightEntity(coordinator, device, config_entry.entry_id) + for device in coordinator.data[dev_type].values() + if device in new_devices + ] + if entities: + async_add_entities(entities) + + config_entry.async_on_unload( + new_device_listener(coordinator, _add_new_entities, LIGHT) + ) class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity): diff --git a/homeassistant/components/comelit/sensor.py b/homeassistant/components/comelit/sensor.py index f47a88723687b0..188ec348ab6069 100644 --- a/homeassistant/components/comelit/sensor.py +++ b/homeassistant/components/comelit/sensor.py @@ -20,6 +20,7 @@ from .coordinator import ComelitConfigEntry, ComelitSerialBridge, ComelitVedoSystem from .entity import ComelitBridgeBaseEntity +from .utils import DeviceType, new_device_listener # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -65,24 +66,22 @@ async def async_setup_bridge_entry( coordinator = cast(ComelitSerialBridge, config_entry.runtime_data) - known_devices: set[int] = set() - - def _check_device() -> None: - current_devices = set(coordinator.data[OTHER]) - new_devices = current_devices - known_devices - if new_devices: - known_devices.update(new_devices) - async_add_entities( - ComelitBridgeSensorEntity( - coordinator, device, config_entry.entry_id, sensor_desc - ) - for sensor_desc in SENSOR_BRIDGE_TYPES - for device in coordinator.data[OTHER].values() - if device.index in new_devices + def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None: + """Add entities for new monitors.""" + entities = [ + ComelitBridgeSensorEntity( + coordinator, device, config_entry.entry_id, sensor_desc ) + for sensor_desc in SENSOR_BRIDGE_TYPES + for device in coordinator.data[dev_type].values() + if device in new_devices + ] + if entities: + async_add_entities(entities) - _check_device() - config_entry.async_on_unload(coordinator.async_add_listener(_check_device)) + config_entry.async_on_unload( + new_device_listener(coordinator, _add_new_entities, OTHER) + ) async def async_setup_vedo_entry( @@ -94,24 +93,22 @@ async def async_setup_vedo_entry( coordinator = cast(ComelitVedoSystem, config_entry.runtime_data) - known_devices: set[int] = set() - - def _check_device() -> None: - current_devices = set(coordinator.data["alarm_zones"]) - new_devices = current_devices - known_devices - if new_devices: - known_devices.update(new_devices) - async_add_entities( - ComelitVedoSensorEntity( - coordinator, device, config_entry.entry_id, sensor_desc - ) - for sensor_desc in SENSOR_VEDO_TYPES - for device in coordinator.data["alarm_zones"].values() - if device.index in new_devices + def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None: + """Add entities for new monitors.""" + entities = [ + ComelitVedoSensorEntity( + coordinator, device, config_entry.entry_id, sensor_desc ) - - _check_device() - config_entry.async_on_unload(coordinator.async_add_listener(_check_device)) + for sensor_desc in SENSOR_VEDO_TYPES + for device in coordinator.data["alarm_zones"].values() + if device in new_devices + ] + if entities: + async_add_entities(entities) + + config_entry.async_on_unload( + new_device_listener(coordinator, _add_new_entities, "alarm_zones") + ) class ComelitBridgeSensorEntity(ComelitBridgeBaseEntity, SensorEntity): diff --git a/homeassistant/components/comelit/switch.py b/homeassistant/components/comelit/switch.py index 076b6091a3dc52..2b15f9533a73c1 100644 --- a/homeassistant/components/comelit/switch.py +++ b/homeassistant/components/comelit/switch.py @@ -13,7 +13,7 @@ from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .entity import ComelitBridgeBaseEntity -from .utils import bridge_api_call +from .utils import DeviceType, bridge_api_call, new_device_listener # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -28,35 +28,20 @@ async def async_setup_entry( coordinator = cast(ComelitSerialBridge, config_entry.runtime_data) - entities: list[ComelitSwitchEntity] = [] - entities.extend( - ComelitSwitchEntity(coordinator, device, config_entry.entry_id) - for device in coordinator.data[IRRIGATION].values() - ) - entities.extend( - ComelitSwitchEntity(coordinator, device, config_entry.entry_id) - for device in coordinator.data[OTHER].values() - ) - async_add_entities(entities) - - known_devices: dict[str, set[int]] = { - dev_type: set() for dev_type in (IRRIGATION, OTHER) - } - - def _check_device() -> None: - for dev_type in (IRRIGATION, OTHER): - current_devices = set(coordinator.data[dev_type]) - new_devices = current_devices - known_devices[dev_type] - if new_devices: - known_devices[dev_type].update(new_devices) - async_add_entities( - ComelitSwitchEntity(coordinator, device, config_entry.entry_id) - for device in coordinator.data[dev_type].values() - if device.index in new_devices - ) - - _check_device() - config_entry.async_on_unload(coordinator.async_add_listener(_check_device)) + def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None: + """Add entities for new monitors.""" + entities = [ + ComelitSwitchEntity(coordinator, device, config_entry.entry_id) + for device in coordinator.data[dev_type].values() + if device in new_devices + ] + if entities: + async_add_entities(entities) + + for dev_type in (IRRIGATION, OTHER): + config_entry.async_on_unload( + new_device_listener(coordinator, _add_new_entities, dev_type) + ) class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity): diff --git a/homeassistant/components/comelit/utils.py b/homeassistant/components/comelit/utils.py index d0f0fbbee3f046..577aa4e2cf16de 100644 --- a/homeassistant/components/comelit/utils.py +++ b/homeassistant/components/comelit/utils.py @@ -4,7 +4,11 @@ from functools import wraps from typing import Any, Concatenate -from aiocomelit import ComelitSerialBridgeObject +from aiocomelit.api import ( + ComelitSerialBridgeObject, + ComelitVedoAreaObject, + ComelitVedoZoneObject, +) from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData from aiohttp import ClientSession, CookieJar @@ -19,8 +23,11 @@ ) from .const import _LOGGER, DOMAIN +from .coordinator import ComelitBaseCoordinator from .entity import ComelitBridgeBaseEntity +DeviceType = ComelitSerialBridgeObject | ComelitVedoAreaObject | ComelitVedoZoneObject + async def async_client_session(hass: HomeAssistant) -> ClientSession: """Return a new aiohttp session.""" @@ -113,3 +120,41 @@ async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: self.coordinator.config_entry.async_start_reauth(self.hass) return cmd_wrapper + + +def new_device_listener( + coordinator: ComelitBaseCoordinator, + new_devices_callback: Callable[ + [ + list[ + ComelitSerialBridgeObject + | ComelitVedoAreaObject + | ComelitVedoZoneObject + ], + str, + ], + None, + ], + data_type: str, +) -> Callable[[], None]: + """Subscribe to coordinator updates to check for new devices.""" + known_devices: set[int] = set() + + def _check_devices() -> None: + """Check for new devices and call callback with any new monitors.""" + if not coordinator.data: + return + + new_devices: list[DeviceType] = [] + for _id in coordinator.data[data_type]: + if _id not in known_devices: + known_devices.add(_id) + new_devices.append(coordinator.data[data_type][_id]) + + if new_devices: + new_devices_callback(new_devices, data_type) + + # Check for devices immediately + _check_devices() + + return coordinator.async_add_listener(_check_devices) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 16a1cbdb4b65d4..8d60528a49f9d5 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -38,9 +38,11 @@ ErrorKey, FuzzyConfig, FuzzyLanguageResponses, + LanguageScores, get_fuzzy_config, get_fuzzy_language, get_intents, + get_language_scores, get_languages, ) import yaml @@ -59,6 +61,7 @@ ) from homeassistant.helpers import ( area_registry as ar, + config_validation as cv, device_registry as dr, entity_registry as er, floor_registry as fr, @@ -343,6 +346,81 @@ async def async_recognize_intent( return result + async def async_debug_recognize( + self, user_input: ConversationInput + ) -> dict[str, Any] | None: + """Debug recognize from user input.""" + result_dict: dict[str, Any] | None = None + + if trigger_result := await self.async_recognize_sentence_trigger(user_input): + result_dict = { + # Matched a user-defined sentence trigger. + # We can't provide the response here without executing the + # trigger. + "match": True, + "source": "trigger", + "sentence_template": trigger_result.sentence_template or "", + } + elif intent_result := await self.async_recognize_intent(user_input): + successful_match = not intent_result.unmatched_entities + result_dict = { + # Name of the matching intent (or the closest) + "intent": { + "name": intent_result.intent.name, + }, + # Slot values that would be received by the intent + "slots": { # direct access to values + entity_key: entity.text or entity.value + for entity_key, entity in intent_result.entities.items() + }, + # Extra slot details, such as the originally matched text + "details": { + entity_key: { + "name": entity.name, + "value": entity.value, + "text": entity.text, + } + for entity_key, entity in intent_result.entities.items() + }, + # Entities/areas/etc. that would be targeted + "targets": {}, + # True if match was successful + "match": successful_match, + # Text of the sentence template that matched (or was closest) + "sentence_template": "", + # When match is incomplete, this will contain the best slot guesses + "unmatched_slots": _get_unmatched_slots(intent_result), + # True if match was not exact + "fuzzy_match": False, + } + + if successful_match: + result_dict["targets"] = { + state.entity_id: {"matched": is_matched} + for state, is_matched in _get_debug_targets( + self.hass, intent_result + ) + } + + if intent_result.intent_sentence is not None: + result_dict["sentence_template"] = intent_result.intent_sentence.text + + if intent_result.intent_metadata: + # Inspect metadata to determine if this matched a custom sentence + if intent_result.intent_metadata.get(METADATA_CUSTOM_SENTENCE): + result_dict["source"] = "custom" + result_dict["file"] = intent_result.intent_metadata.get( + METADATA_CUSTOM_FILE + ) + else: + result_dict["source"] = "builtin" + + result_dict["fuzzy_match"] = intent_result.intent_metadata.get( + METADATA_FUZZY_MATCH, False + ) + + return result_dict + async def _async_handle_message( self, user_input: ConversationInput, @@ -1529,6 +1607,10 @@ async def async_handle_intents( return None return response + async def async_get_language_scores(self) -> dict[str, LanguageScores]: + """Get support scores per language.""" + return await self.hass.async_add_executor_job(get_language_scores) + def _make_error_result( language: str, @@ -1725,3 +1807,75 @@ def _collect_list_references(expression: Expression, list_names: set[str]) -> No elif isinstance(expression, ListReference): # {list} list_names.add(expression.slot_name) + + +def _get_debug_targets( + hass: HomeAssistant, + result: RecognizeResult, +) -> Iterable[tuple[State, bool]]: + """Yield state/is_matched pairs for a hassil recognition.""" + entities = result.entities + + name: str | None = None + area_name: str | None = None + domains: set[str] | None = None + device_classes: set[str] | None = None + state_names: set[str] | None = None + + if "name" in entities: + name = str(entities["name"].value) + + if "area" in entities: + area_name = str(entities["area"].value) + + if "domain" in entities: + domains = set(cv.ensure_list(entities["domain"].value)) + + if "device_class" in entities: + device_classes = set(cv.ensure_list(entities["device_class"].value)) + + if "state" in entities: + # HassGetState only + state_names = set(cv.ensure_list(entities["state"].value)) + + if ( + (name is None) + and (area_name is None) + and (not domains) + and (not device_classes) + and (not state_names) + ): + # Avoid "matching" all entities when there is no filter + return + + states = intent.async_match_states( + hass, + name=name, + area_name=area_name, + domains=domains, + device_classes=device_classes, + ) + + for state in states: + # For queries, a target is "matched" based on its state + is_matched = (state_names is None) or (state.state in state_names) + yield state, is_matched + + +def _get_unmatched_slots( + result: RecognizeResult, +) -> dict[str, str | int | float]: + """Return a dict of unmatched text/range slot entities.""" + unmatched_slots: dict[str, str | int | float] = {} + for entity in result.unmatched_entities_list: + if isinstance(entity, UnmatchedTextEntity): + if entity.text == MISSING_ENTITY: + # Don't report since these are just missing context + # slots. + continue + + unmatched_slots[entity.name] = entity.text + elif isinstance(entity, UnmatchedRangeEntity): + unmatched_slots[entity.name] = entity.value + + return unmatched_slots diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py index c43e6709855986..9d3eb35a7e352f 100644 --- a/homeassistant/components/conversation/http.py +++ b/homeassistant/components/conversation/http.py @@ -2,21 +2,16 @@ from __future__ import annotations -from collections.abc import Iterable from dataclasses import asdict from typing import Any from aiohttp import web -from hassil.recognize import MISSING_ENTITY, RecognizeResult -from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity -from home_assistant_intents import get_language_scores import voluptuous as vol from homeassistant.components import http, websocket_api from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import MATCH_ALL -from homeassistant.core import HomeAssistant, State, callback -from homeassistant.helpers import config_validation as cv, intent +from homeassistant.core import HomeAssistant, callback from homeassistant.util import language as language_util from .agent_manager import ( @@ -26,11 +21,6 @@ get_agent_manager, ) from .const import DATA_COMPONENT -from .default_agent import ( - METADATA_CUSTOM_FILE, - METADATA_CUSTOM_SENTENCE, - METADATA_FUZZY_MATCH, -) from .entity import ConversationEntity from .models import ConversationInput @@ -206,150 +196,12 @@ async def websocket_hass_agent_debug( language=msg.get("language", hass.config.language), agent_id=agent.entity_id, ) - result_dict: dict[str, Any] | None = None - - if trigger_result := await agent.async_recognize_sentence_trigger(user_input): - result_dict = { - # Matched a user-defined sentence trigger. - # We can't provide the response here without executing the - # trigger. - "match": True, - "source": "trigger", - "sentence_template": trigger_result.sentence_template or "", - } - elif intent_result := await agent.async_recognize_intent(user_input): - successful_match = not intent_result.unmatched_entities - result_dict = { - # Name of the matching intent (or the closest) - "intent": { - "name": intent_result.intent.name, - }, - # Slot values that would be received by the intent - "slots": { # direct access to values - entity_key: entity.text or entity.value - for entity_key, entity in intent_result.entities.items() - }, - # Extra slot details, such as the originally matched text - "details": { - entity_key: { - "name": entity.name, - "value": entity.value, - "text": entity.text, - } - for entity_key, entity in intent_result.entities.items() - }, - # Entities/areas/etc. that would be targeted - "targets": {}, - # True if match was successful - "match": successful_match, - # Text of the sentence template that matched (or was closest) - "sentence_template": "", - # When match is incomplete, this will contain the best slot guesses - "unmatched_slots": _get_unmatched_slots(intent_result), - # True if match was not exact - "fuzzy_match": False, - } - - if successful_match: - result_dict["targets"] = { - state.entity_id: {"matched": is_matched} - for state, is_matched in _get_debug_targets(hass, intent_result) - } - - if intent_result.intent_sentence is not None: - result_dict["sentence_template"] = intent_result.intent_sentence.text - - if intent_result.intent_metadata: - # Inspect metadata to determine if this matched a custom sentence - if intent_result.intent_metadata.get(METADATA_CUSTOM_SENTENCE): - result_dict["source"] = "custom" - result_dict["file"] = intent_result.intent_metadata.get( - METADATA_CUSTOM_FILE - ) - else: - result_dict["source"] = "builtin" - - result_dict["fuzzy_match"] = intent_result.intent_metadata.get( - METADATA_FUZZY_MATCH, False - ) - + result_dict = await agent.async_debug_recognize(user_input) result_dicts.append(result_dict) connection.send_result(msg["id"], {"results": result_dicts}) -def _get_debug_targets( - hass: HomeAssistant, - result: RecognizeResult, -) -> Iterable[tuple[State, bool]]: - """Yield state/is_matched pairs for a hassil recognition.""" - entities = result.entities - - name: str | None = None - area_name: str | None = None - domains: set[str] | None = None - device_classes: set[str] | None = None - state_names: set[str] | None = None - - if "name" in entities: - name = str(entities["name"].value) - - if "area" in entities: - area_name = str(entities["area"].value) - - if "domain" in entities: - domains = set(cv.ensure_list(entities["domain"].value)) - - if "device_class" in entities: - device_classes = set(cv.ensure_list(entities["device_class"].value)) - - if "state" in entities: - # HassGetState only - state_names = set(cv.ensure_list(entities["state"].value)) - - if ( - (name is None) - and (area_name is None) - and (not domains) - and (not device_classes) - and (not state_names) - ): - # Avoid "matching" all entities when there is no filter - return - - states = intent.async_match_states( - hass, - name=name, - area_name=area_name, - domains=domains, - device_classes=device_classes, - ) - - for state in states: - # For queries, a target is "matched" based on its state - is_matched = (state_names is None) or (state.state in state_names) - yield state, is_matched - - -def _get_unmatched_slots( - result: RecognizeResult, -) -> dict[str, str | int | float]: - """Return a dict of unmatched text/range slot entities.""" - unmatched_slots: dict[str, str | int | float] = {} - for entity in result.unmatched_entities_list: - if isinstance(entity, UnmatchedTextEntity): - if entity.text == MISSING_ENTITY: - # Don't report since these are just missing context - # slots. - continue - - unmatched_slots[entity.name] = entity.text - elif isinstance(entity, UnmatchedRangeEntity): - unmatched_slots[entity.name] = entity.value - - return unmatched_slots - - @websocket_api.websocket_command( { vol.Required("type"): "conversation/agent/homeassistant/language_scores", @@ -364,10 +216,13 @@ async def websocket_hass_agent_language_scores( msg: dict[str, Any], ) -> None: """Get support scores per language.""" + agent = get_agent_manager(hass).default_agent + assert agent is not None + language = msg.get("language", hass.config.language) country = msg.get("country", hass.config.country) - scores = await hass.async_add_executor_job(get_language_scores) + scores = await agent.async_get_language_scores() matching_langs = language_util.matches(language, scores.keys(), country=country) preferred_lang = matching_langs[0] if matching_langs else language result = { diff --git a/homeassistant/components/nintendo_parental/manifest.json b/homeassistant/components/nintendo_parental/manifest.json index 1e4dbdb342a351..03daba3356d55e 100644 --- a/homeassistant/components/nintendo_parental/manifest.json +++ b/homeassistant/components/nintendo_parental/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["pynintendoparental"], "quality_scale": "bronze", - "requirements": ["pynintendoparental==1.0.1"] + "requirements": ["pynintendoparental==1.1.1"] } diff --git a/homeassistant/components/satel_integra/strings.json b/homeassistant/components/satel_integra/strings.json index 1d6655145b5624..70502902de4314 100644 --- a/homeassistant/components/satel_integra/strings.json +++ b/homeassistant/components/satel_integra/strings.json @@ -24,6 +24,7 @@ }, "config_subentries": { "partition": { + "entry_type": "Partition", "initiate_flow": { "user": "Add partition" }, @@ -57,6 +58,7 @@ } }, "zone": { + "entry_type": "Zone", "initiate_flow": { "user": "Add zone" }, @@ -91,6 +93,7 @@ } }, "output": { + "entry_type": "Output", "initiate_flow": { "user": "Add output" }, @@ -125,6 +128,7 @@ } }, "switchable_output": { + "entry_type": "Switchable output", "initiate_flow": { "user": "Add switchable output" }, diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 5f1f767271b46d..a370b855c4548f 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "silver", - "requirements": ["aioshelly==13.11.0"], + "requirements": ["aioshelly==13.12.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index bc77219f7cb3e3..ce55f6d98ade2c 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -4,7 +4,7 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from aioshelly.block_device import Block from aioshelly.const import RPC_GENERATIONS @@ -37,6 +37,7 @@ ShellySleepingBlockAttributeEntity, async_setup_entry_attribute_entities, async_setup_entry_rpc, + rpc_call, ) from .utils import ( async_remove_orphaned_entities, @@ -78,7 +79,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): is_on: Callable[[dict[str, Any]], bool] method_on: str method_off: str - method_params_fn: Callable[[int | None, bool], dict] + method_params_fn: Callable[[int | None, bool], tuple] RPC_RELAY_SWITCHES = { @@ -87,9 +88,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): sub_key="output", removal_condition=is_rpc_exclude_from_relay, is_on=lambda status: bool(status["output"]), - method_on="Switch.Set", - method_off="Switch.Set", - method_params_fn=lambda id, value: {"id": id, "on": value}, + method_on="switch_set", + method_off="switch_set", + method_params_fn=lambda id, value: (id, value), ), } @@ -101,9 +102,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): config, key, SWITCH_PLATFORM ), is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="generic", ), "boolean_anti_freeze": RpcSwitchDescription( @@ -111,9 +112,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): sub_key="value", entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="anti_freeze", models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT}, ), @@ -121,9 +122,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): key="boolean", sub_key="value", is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="child_lock", models={MODEL_LINKEDGO_ST1820_THERMOSTAT}, ), @@ -132,9 +133,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): sub_key="value", entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="enable", models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT}, ), @@ -142,9 +143,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): key="boolean", sub_key="value", is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="start_charging", models={MODEL_TOP_EV_CHARGER_EVE01}, ), @@ -153,9 +154,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): sub_key="value", entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="state", models={MODEL_NEO_WATER_VALVE}, ), @@ -163,9 +164,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): key="boolean", sub_key="value", is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="zone0", models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER}, ), @@ -173,9 +174,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): key="boolean", sub_key="value", is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="zone1", models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER}, ), @@ -183,9 +184,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): key="boolean", sub_key="value", is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="zone2", models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER}, ), @@ -193,9 +194,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): key="boolean", sub_key="value", is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="zone3", models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER}, ), @@ -203,9 +204,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): key="boolean", sub_key="value", is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="zone4", models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER}, ), @@ -213,9 +214,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): key="boolean", sub_key="value", is_on=lambda status: bool(status["value"]), - method_on="Boolean.Set", - method_off="Boolean.Set", - method_params_fn=lambda id, value: {"id": id, "value": value}, + method_on="boolean_set", + method_off="boolean_set", + method_params_fn=lambda id, value: (id, value), role="zone5", models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER}, ), @@ -223,9 +224,9 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): key="script", sub_key="running", is_on=lambda status: bool(status["running"]), - method_on="Script.Start", - method_off="Script.Stop", - method_params_fn=lambda id, _: {"id": id}, + method_on="script_start", + method_off="script_stop", + method_params_fn=lambda id, _: (id,), entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), @@ -422,19 +423,27 @@ def is_on(self) -> bool: """If switch is on.""" return self.entity_description.is_on(self.status) + @rpc_call async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on relay.""" - await self.call_rpc( - self.entity_description.method_on, - self.entity_description.method_params_fn(self._id, True), - ) + """Turn on switch.""" + method = getattr(self.coordinator.device, self.entity_description.method_on) + + if TYPE_CHECKING: + assert method is not None + + params = self.entity_description.method_params_fn(self._id, True) + await method(*params) + @rpc_call async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off relay.""" - await self.call_rpc( - self.entity_description.method_off, - self.entity_description.method_params_fn(self._id, False), - ) + """Turn off switch.""" + method = getattr(self.coordinator.device, self.entity_description.method_off) + + if TYPE_CHECKING: + assert method is not None + + params = self.entity_description.method_params_fn(self._id, False) + await method(*params) class RpcRelaySwitch(RpcSwitch): diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index aafb05576bf10a..477ee73a0cd747 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -179,6 +179,13 @@ class SmartThingsBinarySensorEntityDescription(BinarySensorEntityDescription): is_on_key="open", ) }, + Capability.GAS_DETECTOR: { + Attribute.GAS: SmartThingsBinarySensorEntityDescription( + key=Attribute.GAS, + device_class=BinarySensorDeviceClass.GAS, + is_on_key="detected", + ) + }, } diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 42581a2807e581..e7f90c2b225a6c 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -530,7 +530,6 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): ) ], }, - # Haven't seen at devices yet Capability.ILLUMINANCE_MEASUREMENT: { Attribute.ILLUMINANCE: [ SmartThingsSensorEntityDescription( @@ -842,7 +841,6 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): ) ] }, - # Haven't seen at devices yet Capability.SIGNAL_STRENGTH: { Attribute.LQI: [ SmartThingsSensorEntityDescription( @@ -1001,7 +999,6 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): ) ], }, - # Haven't seen at devices yet Capability.TVOC_MEASUREMENT: { Attribute.TVOC_LEVEL: [ SmartThingsSensorEntityDescription( @@ -1012,7 +1009,6 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): ) ] }, - # Haven't seen at devices yet Capability.ULTRAVIOLET_INDEX: { Attribute.ULTRAVIOLET_INDEX: [ SmartThingsSensorEntityDescription( diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index c81b5f61354849..8e3ecf97bb80b0 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -87,7 +87,7 @@ "wind_free_sleep": "WindFree sleep", "quiet": "Quiet", "long_wind": "Long wind", - "smart": "Smart", + "smart": "Smart saver", "motion_direct": "Motion direct", "motion_indirect": "Motion indirect" } diff --git a/requirements_all.txt b/requirements_all.txt index a35aa909e9d7f2..e1e8cb043c3083 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -384,7 +384,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==13.11.0 +aioshelly==13.12.0 # homeassistant.components.skybell aioskybell==22.7.0 @@ -2210,7 +2210,7 @@ pynetio==0.1.9.1 pynina==0.3.6 # homeassistant.components.nintendo_parental -pynintendoparental==1.0.1 +pynintendoparental==1.1.1 # homeassistant.components.nobo_hub pynobo==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd497b6bd513eb..a447316f2a70da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -366,7 +366,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==13.11.0 +aioshelly==13.12.0 # homeassistant.components.skybell aioskybell==22.7.0 @@ -1846,7 +1846,7 @@ pynetgear==0.10.10 pynina==0.3.6 # homeassistant.components.nintendo_parental -pynintendoparental==1.0.1 +pynintendoparental==1.1.1 # homeassistant.components.nobo_hub pynobo==1.8.1 diff --git a/tests/components/nintendo_parental/conftest.py b/tests/components/nintendo_parental/conftest.py index cec07e5eb99bd8..7b930589b4b575 100644 --- a/tests/components/nintendo_parental/conftest.py +++ b/tests/components/nintendo_parental/conftest.py @@ -4,7 +4,6 @@ from datetime import datetime from unittest.mock import AsyncMock, MagicMock, patch -from pynintendoparental.device import Device import pytest from homeassistant.components.nintendo_parental.const import DOMAIN @@ -24,18 +23,6 @@ def mock_config_entry() -> MockConfigEntry: ) -@pytest.fixture -def mock_nintendo_device() -> Device: - """Return a mocked device.""" - mock = AsyncMock(spec=Device) - mock.device_id = "testdevid" - mock.name = "Home Assistant Test" - mock.extra = {"device": {"firmwareVersion": {"displayedVersion": "99.99.99"}}} - mock.limit_time = 120 - mock.today_playing_time = 110 - return mock - - @pytest.fixture def mock_nintendo_authenticator() -> Generator[MagicMock]: """Mock Nintendo Authenticator.""" @@ -66,27 +53,6 @@ def mock_nintendo_authenticator() -> Generator[MagicMock]: yield mock_auth -@pytest.fixture -def mock_nintendo_client( - mock_nintendo_device: Device, -) -> Generator[AsyncMock]: - """Mock a Nintendo client.""" - with ( - patch( - "homeassistant.components.nintendo_parental.NintendoParental", - autospec=True, - ) as mock_client, - patch( - "homeassistant.components.nintendo_parental.config_flow.NintendoParental", - new=mock_client, - ), - ): - client = mock_client.return_value - client.update.return_value = True - client.devices.return_value = {"testdevid": mock_nintendo_device} - yield client - - @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" diff --git a/tests/components/nintendo_parental/test_config_flow.py b/tests/components/nintendo_parental/test_config_flow.py index 13216257572f74..7cccf1bf3da98f 100644 --- a/tests/components/nintendo_parental/test_config_flow.py +++ b/tests/components/nintendo_parental/test_config_flow.py @@ -77,7 +77,7 @@ async def test_invalid_auth( # Simulate invalid authentication by raising an exception mock_nintendo_authenticator.complete_login.side_effect = ( - InvalidSessionTokenException + InvalidSessionTokenException(status_code=401, message="Test") ) result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 82eab4bb12dc57..59245c17c08255 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -404,6 +404,7 @@ async def test_rpc_device_services( ) assert (state := hass.states.get(entity_id)) assert state.state == STATE_ON + mock_rpc_device.switch_set.assert_called_once_with(0, True) monkeypatch.setitem(mock_rpc_device.status["switch:0"], "output", False) await hass.services.async_call( @@ -415,6 +416,7 @@ async def test_rpc_device_services( mock_rpc_device.mock_update() assert (state := hass.states.get(entity_id)) assert state.state == STATE_OFF + mock_rpc_device.switch_set.assert_called_with(0, False) async def test_rpc_device_unique_ids( @@ -507,7 +509,7 @@ async def test_rpc_set_state_errors( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC device set state connection/call errors.""" - monkeypatch.setattr(mock_rpc_device, "call_rpc", AsyncMock(side_effect=exc)) + mock_rpc_device.switch_set.side_effect = exc monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) await init_integration(hass, 2) @@ -525,11 +527,7 @@ async def test_rpc_auth_error( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test RPC device set state authentication error.""" - monkeypatch.setattr( - mock_rpc_device, - "call_rpc", - AsyncMock(side_effect=InvalidAuthError), - ) + mock_rpc_device.switch_set.side_effect = InvalidAuthError monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) entry = await init_integration(hass, 2) @@ -657,6 +655,7 @@ async def test_rpc_device_virtual_switch( mock_rpc_device.mock_update() assert (state := hass.states.get(entity_id)) assert state.state == STATE_OFF + mock_rpc_device.boolean_set.assert_called_once_with(200, False) monkeypatch.setitem(mock_rpc_device.status["boolean:200"], "value", True) await hass.services.async_call( @@ -668,6 +667,7 @@ async def test_rpc_device_virtual_switch( mock_rpc_device.mock_update() assert (state := hass.states.get(entity_id)) assert state.state == STATE_ON + mock_rpc_device.boolean_set.assert_called_with(200, True) @pytest.mark.usefixtures("disable_async_remove_shelly_rpc_entities") @@ -815,6 +815,7 @@ async def test_rpc_device_script_switch( assert (state := hass.states.get(entity_id)) assert state.state == STATE_OFF + mock_rpc_device.script_stop.assert_called_once_with(1) monkeypatch.setitem(mock_rpc_device.status[key], "running", True) await hass.services.async_call( @@ -827,3 +828,4 @@ async def test_rpc_device_script_switch( assert (state := hass.states.get(entity_id)) assert state.state == STATE_ON + mock_rpc_device.script_start.assert_called_once_with(1) diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index a68bbba22d215b..393b6b4e3d6741 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -97,6 +97,7 @@ def mock_smartthings() -> Generator[AsyncMock]: @pytest.fixture( params=[ "aq_sensor_3_ikea", + "aeotec_ms6", "da_ac_airsensor_01001", "da_ac_rac_000001", "da_ac_rac_000003", @@ -156,6 +157,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "heatit_ztrm3_thermostat", "heatit_zpushwall", "generic_ef00_v1", + "gas_detector", "bosch_radiator_thermostat_ii", "im_speaker_ai_0001", "im_smarttag2_ble_uwb", diff --git a/tests/components/smartthings/fixtures/device_status/aeotec_ms6.json b/tests/components/smartthings/fixtures/device_status/aeotec_ms6.json new file mode 100644 index 00000000000000..8e80dbae1ef3fe --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/aeotec_ms6.json @@ -0,0 +1,62 @@ +{ + "components": { + "main": { + "ultravioletIndex": { + "ultravioletIndex": { + "value": 0, + "timestamp": "2025-09-30T15:13:46.521Z" + } + }, + "relativeHumidityMeasurement": { + "humidity": { + "value": 60.0, + "unit": "%", + "timestamp": "2025-09-30T15:13:45.441Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 22.2, + "unit": "C", + "timestamp": "2025-09-30T16:13:50.478Z" + } + }, + "refresh": {}, + "motionSensor": { + "motion": { + "value": "inactive", + "timestamp": "2025-09-30T15:33:27.594Z" + } + }, + "illuminanceMeasurement": { + "illuminance": { + "value": 30, + "unit": "lux", + "timestamp": "2025-09-30T15:13:52.607Z" + } + }, + "battery": { + "quantity": { + "value": null + }, + "battery": { + "value": 100, + "unit": "%", + "timestamp": "2025-09-30T15:13:46.166Z" + }, + "type": { + "value": null + } + }, + "tamperAlert": { + "tamper": { + "value": "clear", + "timestamp": "2025-09-30T14:06:07.064Z" + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/device_status/gas_detector.json b/tests/components/smartthings/fixtures/device_status/gas_detector.json new file mode 100644 index 00000000000000..eac3b5b4548658 --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/gas_detector.json @@ -0,0 +1,25 @@ +{ + "components": { + "main": { + "momentary": {}, + "gasDetector": { + "gas": { + "value": "clear", + "timestamp": "2025-10-02T03:18:27.139Z" + } + }, + "signalStrength": { + "rssi": { + "value": -71, + "unit": "dBm", + "timestamp": "2025-10-07T04:17:08.419Z" + }, + "lqi": { + "value": 148, + "timestamp": "2025-10-07T04:32:08.512Z" + } + }, + "refresh": {} + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/aeotec_ms6.json b/tests/components/smartthings/fixtures/devices/aeotec_ms6.json new file mode 100644 index 00000000000000..3ac2ca1a8c2bb2 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/aeotec_ms6.json @@ -0,0 +1,86 @@ +{ + "items": [ + { + "deviceId": "00f9233e-fdaa-4020-99d4-e0073e53996a", + "name": "aeotec-ms6", + "label": "Parent's Bedroom Sensor", + "manufacturerName": "SmartThingsCommunity", + "presentationId": "6d160aa8-7f54-3611-b7de-0b335d162529", + "deviceManufacturerCode": "0086-0102-0064", + "locationId": "3478ae40-8bd4-40b8-b7e6-f25e3cf86409", + "ownerId": "fe7f9079-8e23-8307-fb7e-4d58929391cf", + "roomId": "f1bb7871-3a3d-48da-b23f-0e1297e8acb0", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "motionSensor", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "relativeHumidityMeasurement", + "version": 1 + }, + { + "id": "illuminanceMeasurement", + "version": 1 + }, + { + "id": "ultravioletIndex", + "version": 1 + }, + { + "id": "tamperAlert", + "version": 1 + }, + { + "id": "battery", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + } + ], + "categories": [ + { + "name": "MotionSensor", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2025-04-17T05:47:05.803Z", + "parentDeviceId": "9fdfde11-206e-47af-9e47-9c314d8d965f", + "profile": { + "id": "9893d370-2af6-32a0-86c5-f1a6d2b9fea7" + }, + "zwave": { + "networkId": "BE", + "driverId": "42930682-019d-4dbe-8098-760d7afb3c7f", + "executingLocally": true, + "hubId": "9fdfde11-206e-47af-9e47-9c314d8d965f", + "networkSecurityLevel": "ZWAVE_LEGACY_NON_SECURE", + "provisioningState": "PROVISIONED", + "manufacturerId": 134, + "productType": 258, + "productId": 100, + "fingerprintType": "ZWAVE_MANUFACTURER", + "fingerprintId": "Aeotec/MS6/US" + }, + "type": "ZWAVE", + "restrictionTier": 0, + "allowed": null, + "executionContext": "LOCAL", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/fixtures/devices/gas_detector.json b/tests/components/smartthings/fixtures/devices/gas_detector.json new file mode 100644 index 00000000000000..9ab0574c706f13 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/gas_detector.json @@ -0,0 +1,66 @@ +{ + "items": [ + { + "deviceId": "d830b46f-f094-4560-b8c3-7690032fdb4c", + "name": "generic-ef00-v1", + "label": "Gas Detector", + "manufacturerName": "SmartThingsCommunity", + "presentationId": "d4b88195-fd5b-39d3-ac6f-7070655f08ab", + "deviceManufacturerCode": "_TZE284_chbyv06x", + "locationId": "7139bb09-31e3-4fad-bf03-b9ad02e57b41", + "ownerId": "00126705-d35b-27ee-d18b-17620d9929e7", + "roomId": "5adccb3a-8ae7-41c0-bc58-7ba80ff78a18", + "components": [ + { + "id": "main", + "label": "Detector", + "capabilities": [ + { + "id": "gasDetector", + "version": 1 + }, + { + "id": "momentary", + "version": 1 + }, + { + "id": "signalStrength", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + } + ], + "categories": [ + { + "name": "Siren", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2025-05-25T04:55:42.440Z", + "profile": { + "id": "1d34dd9d-6840-3df6-a6d0-5d9f4a4af2e1" + }, + "zigbee": { + "eui": "A4C138C524A5BC8D", + "networkId": "1575", + "driverId": "bc7fd1bc-eb00-4b7f-8977-172acf823508", + "executingLocally": true, + "hubId": "0afe704f-eabb-4e4d-8333-6c73903e4f84", + "provisioningState": "DRIVER_SWITCH", + "fingerprintType": "ZIGBEE_GENERIC", + "fingerprintId": "GenericEF00" + }, + "type": "ZIGBEE", + "restrictionTier": 0, + "allowed": null, + "executionContext": "LOCAL", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_binary_sensor.ambr b/tests/components/smartthings/snapshots/test_binary_sensor.ambr index 4637de49efbc67..4873737f7f536e 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -1,4 +1,102 @@ # serializer version: 1 +# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.parent_s_bedroom_sensor_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_motionSensor_motion_motion', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': "Parent's Bedroom Sensor Motion", + }), + 'context': , + 'entity_id': 'binary_sensor.parent_s_bedroom_sensor_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.parent_s_bedroom_sensor_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_tamperAlert_tamper_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': "Parent's Bedroom Sensor Tamper", + }), + 'context': , + 'entity_id': 'binary_sensor.parent_s_bedroom_sensor_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[c2c_arlo_pro_3_switch][binary_sensor.2nd_floor_hallway_motion-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2572,6 +2670,55 @@ 'state': 'off', }) # --- +# name: test_all_entities[gas_detector][binary_sensor.gas_detector_gas-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.gas_detector_gas', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Gas', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_gasDetector_gas_gas', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[gas_detector][binary_sensor.gas_detector_gas-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'Gas Detector Gas', + }), + 'context': , + 'entity_id': 'binary_sensor.gas_detector_gas', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[iphone][binary_sensor.iphone_presence-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index 4caa2952cbc0c5..3de46994c4c257 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -64,6 +64,37 @@ 'via_device_id': None, }) # --- +# name: test_devices[aeotec_ms6] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + '00f9233e-fdaa-4020-99d4-e0073e53996a', + ), + }), + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': None, + 'name': "Parent's Bedroom Sensor", + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[aq_sensor_3_ikea] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -1304,6 +1335,37 @@ 'via_device_id': None, }) # --- +# name: test_devices[gas_detector] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + 'd830b46f-f094-4560-b8c3-7690032fdb4c', + ), + }), + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': None, + 'name': 'Gas Detector', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[gas_meter] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 5e9d093eb791d1..bf5760afc3dbb0 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -163,6 +163,269 @@ 'state': 'unknown', }) # --- +# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.parent_s_bedroom_sensor_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_battery_battery_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': "Parent's Bedroom Sensor Battery", + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.parent_s_bedroom_sensor_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.parent_s_bedroom_sensor_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_relativeHumidityMeasurement_humidity_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': "Parent's Bedroom Sensor Humidity", + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.parent_s_bedroom_sensor_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.0', + }) +# --- +# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_illuminance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.parent_s_bedroom_sensor_illuminance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_illuminanceMeasurement_illuminance_illuminance', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_illuminance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': "Parent's Bedroom Sensor Illuminance", + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.parent_s_bedroom_sensor_illuminance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) +# --- +# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.parent_s_bedroom_sensor_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': "Parent's Bedroom Sensor Temperature", + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.parent_s_bedroom_sensor_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.2', + }) +# --- +# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_uv_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.parent_s_bedroom_sensor_uv_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'UV index', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index', + 'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_ultravioletIndex_ultravioletIndex_ultravioletIndex', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_uv_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': "Parent's Bedroom Sensor UV index", + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.parent_s_bedroom_sensor_uv_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -12651,6 +12914,110 @@ 'state': 'unknown', }) # --- +# name: test_all_entities[gas_detector][sensor.gas_detector_link_quality-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gas_detector_link_quality', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Link quality', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'link_quality', + 'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_signalStrength_lqi_lqi', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[gas_detector][sensor.gas_detector_link_quality-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Gas Detector Link quality', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.gas_detector_link_quality', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '148', + }) +# --- +# name: test_all_entities[gas_detector][sensor.gas_detector_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gas_detector_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Signal strength', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_signalStrength_rssi_rssi', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_all_entities[gas_detector][sensor.gas_detector_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Gas Detector Signal strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.gas_detector_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-71', + }) +# --- # name: test_all_entities[gas_meter][sensor.gas_meter_gas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({