diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index efbc611f9d389d..e16a86c3d4f2d3 100644 --- a/homeassistant/components/adax/manifest.json +++ b/homeassistant/components/adax/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/adax", "iot_class": "local_polling", "loggers": ["adax", "adax_local"], - "requirements": ["adax==0.4.0", "Adax-local==0.1.5"] + "requirements": ["adax==0.4.0", "Adax-local==0.2.0"] } diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 787014c0cc8742..6ea51075e88022 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -542,7 +542,16 @@ async def _async_create_entry(self) -> ConfigFlowResult: # Check if Z-Wave capabilities are present and start discovery flow next_flow_id: str | None = None - if self._device_info.zwave_proxy_feature_flags: + # If the zwave_home_id is not set, we don't know if it's a fresh + # adapter, or the cable is just unplugged. So only start + # the zwave_js config flow automatically if there is a + # zwave_home_id present. If it's a fresh adapter, the manager + # will handle starting the flow once it gets the home id changed + # request from the ESPHome device. + if ( + self._device_info.zwave_proxy_feature_flags + and self._device_info.zwave_home_id + ): assert self._connected_address is not None assert self._port is not None @@ -559,7 +568,7 @@ async def _async_create_entry(self) -> ConfigFlowResult: }, data=ESPHomeServiceInfo( name=self._device_info.name, - zwave_home_id=self._device_info.zwave_home_id or None, + zwave_home_id=self._device_info.zwave_home_id, ip_address=self._connected_address, port=self._port, noise_psk=self._noise_psk, diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index f329d8ba11a8a1..de2c74479bae45 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -491,13 +491,30 @@ def async_on_connect( assert self.client.connected_address + # If the device does not have a zwave_home_id, it means + # either the Z-Wave controller has never been connected + # to the ESPHome device, or the Z-Wave controller has + # never been provisioned with a home ID (brand new). + # Since we cannot tell the difference, and it could + # just be the cable is unplugged we only + # automatically start the flow if we have a home ID. + if not device_info.zwave_home_id: + return + + self.async_create_zwave_js_flow(hass, device_info, device_info.zwave_home_id) + + def async_create_zwave_js_flow( + self, hass: HomeAssistant, device_info: DeviceInfo, zwave_home_id: int + ) -> None: + """Create a zwave_js config flow for a Z-Wave JS Proxy device.""" + assert self.client.connected_address is not None discovery_flow.async_create_flow( hass, "zwave_js", {"source": config_entries.SOURCE_ESPHOME}, ESPHomeServiceInfo( name=device_info.name, - zwave_home_id=device_info.zwave_home_id or None, + zwave_home_id=zwave_home_id, ip_address=self.client.connected_address, port=self.client.port, noise_psk=self.client.noise_psk, diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 239dfe5662ac03..6e3c8b156b2acb 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -6,6 +6,7 @@ from functools import partial import logging import secrets +import struct from typing import TYPE_CHECKING, Any, NamedTuple from aioesphomeapi import ( @@ -22,6 +23,8 @@ RequiresEncryptionAPIError, UserService, UserServiceArgType, + ZWaveProxyRequest, + ZWaveProxyRequestType, parse_log_message, ) from awesomeversion import AwesomeVersion @@ -84,6 +87,8 @@ from .entry_data import ESPHomeConfigEntry, RuntimeEntryData DEVICE_CONFLICT_ISSUE_FORMAT = "device_conflict-{}" +UNPACK_UINT32_BE = struct.Struct(">I").unpack_from + if TYPE_CHECKING: from aioesphomeapi.api_pb2 import SubscribeLogsResponse # type: ignore[attr-defined] # noqa: I001 @@ -557,6 +562,11 @@ async def _on_connect(self) -> None: ) entry_data.loaded_platforms.add(Platform.ASSIST_SATELLITE) + if device_info.zwave_proxy_feature_flags: + entry_data.disconnect_callbacks.add( + cli.subscribe_zwave_proxy_request(self._async_zwave_proxy_request) + ) + cli.subscribe_home_assistant_states_and_services( on_state=entry_data.async_update_state, on_service_call=self.async_on_service_call, @@ -568,6 +578,25 @@ async def _on_connect(self) -> None: _async_check_firmware_version(hass, device_info, api_version) _async_check_using_api_password(hass, device_info, bool(self.password)) + def _async_zwave_proxy_request(self, request: ZWaveProxyRequest) -> None: + """Handle a request to create a zwave_js config flow.""" + if request.type != ZWaveProxyRequestType.HOME_ID_CHANGE: + return + # ESPHome will send a home id change on every connection + # if the Z-Wave controller is connected to the ESPHome device + # so we know for sure that the Z-Wave controller is connected + # when we get the message. This makes it safe to start + # the zwave_js config flow automatically even if the zwave_home_id + # is 0 (not yet provisioned) as we know for sure the controller + # is connected to the ESPHome device and do not have to guess + # if it's a broken connection or Z-Wave controller or a not + # yet provisioned controller. + zwave_home_id: int = UNPACK_UINT32_BE(request.data[0:4])[0] + assert self.entry_data.device_info is not None + self.entry_data.async_create_zwave_js_flow( + self.hass, self.entry_data.device_info, zwave_home_id + ) + async def on_disconnect(self, expected_disconnect: bool) -> None: """Run disconnect callbacks on API disconnect.""" entry_data = self.entry_data diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index bd1a6f890a36aa..eb6f7716f4688b 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -4,6 +4,7 @@ "codeowners": ["@mib1185"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/feedreader", + "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["feedparser", "sgmllib3k"], "requirements": ["feedparser==6.0.12"] diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index bfeef29ceba7ae..6ddd0eff51fce7 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from collections.abc import Callable, Mapping from dataclasses import dataclass, field from datetime import datetime, timedelta @@ -16,6 +17,7 @@ FritzConnectionException, FritzSecurityError, ) +from fritzconnection.lib.fritzcall import FritzCall from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus from fritzconnection.lib.fritzwlan import FritzGuestWLAN @@ -120,6 +122,7 @@ def __init__( self.fritz_guest_wifi: FritzGuestWLAN = None self.fritz_hosts: FritzHosts = None self.fritz_status: FritzStatus = None + self.fritz_call: FritzCall = None self.host = host self.mesh_role = MeshRoles.NONE self.mesh_wifi_uplink = False @@ -183,6 +186,7 @@ def setup(self) -> None: self.fritz_hosts = FritzHosts(fc=self.connection) self.fritz_guest_wifi = FritzGuestWLAN(fc=self.connection) self.fritz_status = FritzStatus(fc=self.connection) + self.fritz_call = FritzCall(fc=self.connection) info = self.fritz_status.get_device_info() _LOGGER.debug( @@ -617,6 +621,14 @@ async def async_trigger_set_guest_password( self.fritz_guest_wifi.set_password, password, length ) + async def async_trigger_dial(self, number: str, max_ring_seconds: int) -> None: + """Trigger service to dial a number.""" + try: + await self.hass.async_add_executor_job(self.fritz_call.dial, number) + await asyncio.sleep(max_ring_seconds) + finally: + await self.hass.async_add_executor_job(self.fritz_call.hangup) + async def async_trigger_cleanup(self) -> None: """Trigger device trackers cleanup.""" _LOGGER.debug("Device tracker cleanup triggered") diff --git a/homeassistant/components/fritz/icons.json b/homeassistant/components/fritz/icons.json index 481568a4c2ce33..436e4a5781c575 100644 --- a/homeassistant/components/fritz/icons.json +++ b/homeassistant/components/fritz/icons.json @@ -62,6 +62,9 @@ }, "set_guest_wifi_password": { "service": "mdi:form-textbox-password" + }, + "dial": { + "service": "mdi:phone-dial" } } } diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index fa5b2fc46127c8..8f62d59d2406a1 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -5,6 +5,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/fritz", + "integration_type": "hub", "iot_class": "local_polling", "loggers": ["fritzconnection"], "requirements": ["fritzconnection[qr]==1.15.0", "xmltodict==0.13.0"], diff --git a/homeassistant/components/fritz/services.py b/homeassistant/components/fritz/services.py index 43d10ee7f0aa93..9d7d6b339b2aa8 100644 --- a/homeassistant/components/fritz/services.py +++ b/homeassistant/components/fritz/services.py @@ -4,6 +4,7 @@ from fritzconnection.core.exceptions import ( FritzActionError, + FritzActionFailedError, FritzConnectionException, FritzServiceError, ) @@ -27,6 +28,14 @@ vol.Optional("length"): vol.Range(min=8, max=63), } ) +SERVICE_DIAL = "dial" +SERVICE_SCHEMA_DIAL = vol.Schema( + { + vol.Required("device_id"): str, + vol.Required("number"): str, + vol.Required("max_ring_seconds"): vol.Range(min=1, max=300), + } +) async def _async_set_guest_wifi_password(service_call: ServiceCall) -> None: @@ -65,6 +74,46 @@ async def _async_set_guest_wifi_password(service_call: ServiceCall) -> None: ) from ex +async def _async_dial(service_call: ServiceCall) -> None: + """Call Fritz dial service.""" + target_entry_ids = await async_extract_config_entry_ids(service_call) + target_entries: list[FritzConfigEntry] = [ + loaded_entry + for loaded_entry in service_call.hass.config_entries.async_loaded_entries( + DOMAIN + ) + if loaded_entry.entry_id in target_entry_ids + ] + + if not target_entries: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="config_entry_not_found", + translation_placeholders={"service": service_call.service}, + ) + + for target_entry in target_entries: + _LOGGER.debug("Executing service %s", service_call.service) + avm_wrapper = target_entry.runtime_data + try: + await avm_wrapper.async_trigger_dial( + service_call.data["number"], + max_ring_seconds=service_call.data["max_ring_seconds"], + ) + except (FritzServiceError, FritzActionError) as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="service_parameter_unknown" + ) from ex + except FritzActionFailedError as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="service_dial_failed" + ) from ex + except FritzConnectionException as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="service_not_supported" + ) from ex + + @callback def async_setup_services(hass: HomeAssistant) -> None: """Set up services for Fritz integration.""" @@ -75,3 +124,4 @@ def async_setup_services(hass: HomeAssistant) -> None: _async_set_guest_wifi_password, SERVICE_SCHEMA_SET_GUEST_WIFI_PW, ) + hass.services.async_register(DOMAIN, SERVICE_DIAL, _async_dial, SERVICE_SCHEMA_DIAL) diff --git a/homeassistant/components/fritz/services.yaml b/homeassistant/components/fritz/services.yaml index 0ac7ca20c3d5b7..815c3b2487452b 100644 --- a/homeassistant/components/fritz/services.yaml +++ b/homeassistant/components/fritz/services.yaml @@ -17,3 +17,24 @@ set_guest_wifi_password: number: min: 8 max: 63 +dial: + fields: + device_id: + required: true + selector: + device: + integration: fritz + entity: + device_class: connectivity + number: + required: true + selector: + text: + max_ring_seconds: + default: 15 + required: true + selector: + number: + min: 1 + max: 300 + unit_of_measurement: seconds diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 5ff8dd37d332b0..6d49f09b9a6d00 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -198,12 +198,33 @@ "description": "Length of the new password. It will be auto-generated if no password is set." } } + }, + "dial": { + "name": "Dial a phone number", + "description": "Makes the FRITZ!Box dial a phone number.", + "fields": { + "device_id": { + "name": "FRITZ!Box device", + "description": "Select the FRITZ!Box to dial from." + }, + "number": { + "name": "Phone number", + "description": "The phone number to dial." + }, + "max_ring_seconds": { + "name": "Maximum ring duration", + "description": "The maximum number of seconds to ring after dialing." + } + } } }, "exceptions": { "config_entry_not_found": { "message": "Failed to perform action \"{service}\". Config entry for target not found" }, + "service_dial_failed": { + "message": "Failed to dial, check if the click to dial service of the FRITZ!Box is activated" + }, "service_parameter_unknown": { "message": "Action or parameter unknown" }, diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index bccc00250048ba..5e27fc1ed794a4 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.10.0", "xknxproject==3.8.2", - "knx-frontend==2025.10.9.185845" + "knx-frontend==2025.10.17.202411" ], "single_config_entry": true } diff --git a/homeassistant/components/knx/storage/entity_store_schema.py b/homeassistant/components/knx/storage/entity_store_schema.py index 934008132a8975..a764ea92e446b8 100644 --- a/homeassistant/components/knx/storage/entity_store_schema.py +++ b/homeassistant/components/knx/storage/entity_store_schema.py @@ -93,7 +93,6 @@ BINARY_SENSOR_KNX_SCHEMA = vol.Schema( { - "section_binary_sensor": KNXSectionFlat(), vol.Required(CONF_GA_SENSOR): GASelector( write=False, state_required=True, valid_dpt="1" ), @@ -117,10 +116,8 @@ COVER_KNX_SCHEMA = AllSerializeFirst( vol.Schema( { - "section_binary_control": KNXSectionFlat(), vol.Optional(CONF_GA_UP_DOWN): GASelector(state=False, valid_dpt="1"), vol.Optional(CoverConf.INVERT_UPDOWN): selector.BooleanSelector(), - "section_stop_control": KNXSectionFlat(), vol.Optional(CONF_GA_STOP): GASelector(state=False, valid_dpt="1"), vol.Optional(CONF_GA_STEP): GASelector(state=False, valid_dpt="1"), "section_position_control": KNXSectionFlat(collapsible=True), @@ -195,11 +192,9 @@ class LightColorMode(StrEnum): LIGHT_KNX_SCHEMA = AllSerializeFirst( vol.Schema( { - "section_switch": KNXSectionFlat(), vol.Optional(CONF_GA_SWITCH): GASelector( write_required=True, valid_dpt="1" ), - "section_brightness": KNXSectionFlat(), vol.Optional(CONF_GA_BRIGHTNESS): GASelector( write_required=True, valid_dpt="5.001" ), @@ -229,28 +224,24 @@ class LightColorMode(StrEnum): GroupSelectOption( translation_key="individual_addresses", schema={ - "section_red": KNXSectionFlat(), vol.Optional(CONF_GA_RED_SWITCH): GASelector( write_required=False, valid_dpt="1" ), vol.Required(CONF_GA_RED_BRIGHTNESS): GASelector( write_required=True, valid_dpt="5.001" ), - "section_green": KNXSectionFlat(), vol.Optional(CONF_GA_GREEN_SWITCH): GASelector( write_required=False, valid_dpt="1" ), vol.Required(CONF_GA_GREEN_BRIGHTNESS): GASelector( write_required=True, valid_dpt="5.001" ), - "section_blue": KNXSectionFlat(), vol.Optional(CONF_GA_BLUE_SWITCH): GASelector( write_required=False, valid_dpt="1" ), vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector( write_required=True, valid_dpt="5.001" ), - "section_white": KNXSectionFlat(), vol.Optional(CONF_GA_WHITE_SWITCH): GASelector( write_required=False, valid_dpt="1" ), @@ -313,7 +304,6 @@ class LightColorMode(StrEnum): SWITCH_KNX_SCHEMA = vol.Schema( { - "section_switch": KNXSectionFlat(), vol.Required(CONF_GA_SWITCH): GASelector(write_required=True, valid_dpt="1"), vol.Optional(CONF_INVERT, default=False): selector.BooleanSelector(), vol.Optional(CONF_RESPOND_TO_READ, default=False): selector.BooleanSelector(), diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index df7ddc86e8c443..48c3ce12027acb 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -386,9 +386,9 @@ "binary_sensor": { "description": "Read-only entity for binary datapoints. Window or door states etc.", "knx": { - "section_binary_sensor": { - "title": "Binary sensor", - "description": "DPT 1 group addresses representing binary states." + "ga_sensor": { + "label": "State", + "description": "Group address representing a binary state." }, "invert": { "label": "Invert", @@ -415,50 +415,47 @@ "cover": { "description": "The KNX cover platform is used as an interface to shutter actuators.", "knx": { - "section_binary_control": { - "title": "Open/Close control", - "description": "DPT 1 group addresses triggering full movement." - }, "ga_up_down": { - "label": "Open/Close" + "label": "Open/Close control", + "description": "Group addresses triggering a full movement." }, "invert_updown": { - "label": "Invert", + "label": "Invert open/close", "description": "Default is UP (0) to open a cover and DOWN (1) to close a cover. Enable this to invert the open/close commands from/to your KNX actuator." }, - "section_stop_control": { - "title": "Stop", - "description": "DPT 1 group addresses for stopping movement." - }, "ga_stop": { - "label": "Stop" + "label": "Stop", + "description": "Group addresses for stopping movement." }, "ga_step": { - "label": "Stepwise move" + "label": "Stepwise move", + "description": "Group addresses for stepwise movement. Used to stop the cover when no dedicated stop address is available." }, "section_position_control": { "title": "Position", - "description": "DPT 5 group addresses for cover position." + "description": "Control cover position." }, "ga_position_set": { - "label": "Set position" + "label": "Set position", + "description": "Group addresses for setting a new absolute position." }, "ga_position_state": { - "label": "Current position" + "label": "Current position", + "description": "Group addresses reporting the current position." }, "invert_position": { - "label": "Invert", - "description": "Invert payload before processing. Enable if KNX reports 0% as fully closed." + "label": "Invert position", + "description": "Invert telegram payload before processing. Enable if KNX reports 0% as fully closed." }, "section_tilt_control": { "title": "Tilt", - "description": "DPT 5 group addresses for slat tilt angle." + "description": "Control slat tilt angle." }, "ga_angle": { "label": "Tilt angle" }, "invert_angle": { - "label": "[%key:component::knx::config_panel::entities::create::cover::knx::invert_position::label%]", + "label": "Invert angle", "description": "[%key:component::knx::config_panel::entities::create::cover::knx::invert_position::description%]" }, "section_travel_time": { @@ -466,38 +463,32 @@ "description": "Used to calculate intermediate positions of the cover while traveling." }, "travelling_time_up": { - "label": "Travel time for opening", - "description": "Time the cover needs to fully open in seconds." + "label": "Time for opening", + "description": "Time in seconds the cover needs to fully open." }, "travelling_time_down": { - "label": "Travel time for closing", - "description": "Time the cover needs to fully close in seconds." + "label": "Time for closing", + "description": "Time in seconds the cover needs to fully close." } } }, "light": { "description": "The KNX light platform is used as an interface to dimming actuators, LED controllers, DALI gateways and similar.", "knx": { - "section_switch": { - "title": "Switch", - "description": "Turn the light on/off." - }, "ga_switch": { - "label": "Switch" - }, - "section_brightness": { - "title": "Brightness", - "description": "Control the brightness of the light." + "label": "Switch", + "description": "Turn the light on/off." }, "ga_brightness": { - "label": "Brightness" + "label": "Brightness", + "description": "Control the absolute brightness of the light." }, "section_color_temp": { "title": "Color temperature", "description": "Control the color temperature of the light." }, "ga_color_temp": { - "label": "Color temperature", + "label": "Color temperature addresses", "options": { "5_001": "Percent", "7_600": "Kelvin", @@ -520,60 +511,52 @@ }, "individual_addresses": { "label": "Individual addresses", - "description": "RGB(W) using individual state and brightness group addresses." + "description": "RGB(W) using individual group addresses for each color channel's state and brightness." }, "hsv_addresses": { "label": "HSV", - "description": "Hue, saturation and brightness using individual group addresses." + "description": "Hue, saturation and brightness controlled by individual group addresses." } }, "ga_color": { - "label": "Color", + "label": "Color addresses", "options": { "232_600": "RGB", "242_600": "XYY", "251_600": "RGBW" } }, - "section_red": { - "title": "Red", - "description": "Controls the light's red color component. Brightness group address is required." - }, "ga_red_switch": { - "label": "Red switch" + "label": "Red switch", + "description": "Group address to switch the color channel on/off." }, "ga_red_brightness": { - "label": "Red brightness" - }, - "section_green": { - "title": "Green", - "description": "Controls the light's green color component. Brightness group address is required." + "label": "Red brightness", + "description": "Group address to control the brightness of the color channel. Required." }, "ga_green_switch": { - "label": "Green switch" + "label": "Green switch", + "description": "[%key:component::knx::config_panel::entities::create::light::knx::color::ga_red_switch::description%]" }, "ga_green_brightness": { - "label": "Green brightness" - }, - "section_blue": { - "title": "Blue", - "description": "Controls the light's blue color component. Brightness group address is required." + "label": "Green brightness", + "description": "[%key:component::knx::config_panel::entities::create::light::knx::color::ga_red_brightness::description%]" }, "ga_blue_switch": { - "label": "Blue switch" + "label": "Blue switch", + "description": "[%key:component::knx::config_panel::entities::create::light::knx::color::ga_red_switch::description%]" }, "ga_blue_brightness": { - "label": "Blue brightness" - }, - "section_white": { - "title": "White", - "description": "Controls the light's white color component. Brightness group address is required." + "label": "Blue brightness", + "description": "[%key:component::knx::config_panel::entities::create::light::knx::color::ga_red_brightness::description%]" }, "ga_white_switch": { - "label": "White switch" + "label": "White switch", + "description": "[%key:component::knx::config_panel::entities::create::light::knx::color::ga_red_switch::description%]" }, "ga_white_brightness": { - "label": "White brightness" + "label": "White brightness", + "description": "Group address to control the brightness of the color channel." }, "ga_hue": { "label": "Hue", @@ -589,10 +572,6 @@ "switch": { "description": "The KNX switch platform is used as an interface to switching actuators.", "knx": { - "section_switch": { - "title": "Switching", - "description": "DPT 1 group addresses controlling the switch function." - }, "ga_switch": { "label": "Switch", "description": "Group address to switch the device on/off." diff --git a/homeassistant/components/openrgb/__init__.py b/homeassistant/components/openrgb/__init__.py index 320a5aeebc5871..6c13210f02197f 100644 --- a/homeassistant/components/openrgb/__init__.py +++ b/homeassistant/components/openrgb/__init__.py @@ -5,6 +5,7 @@ from homeassistant.const import CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceEntry from .const import DOMAIN from .coordinator import OpenRGBConfigEntry, OpenRGBCoordinator @@ -48,3 +49,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenRGBConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, entry: OpenRGBConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, entry: OpenRGBConfigEntry, device_entry: DeviceEntry +) -> bool: + """Allows removal of device if it is no longer connected.""" + coordinator = entry.runtime_data + + for domain, identifier in device_entry.identifiers: + if domain != DOMAIN: + continue + + # Block removal of the OpenRGB SDK Server device + if identifier == entry.entry_id: + return False + + # Block removal of the OpenRGB device if it is still connected + if identifier in coordinator.data: + return False + + # Device is not connected or is not an OpenRGB device, allow removal + return True diff --git a/homeassistant/components/probe_plus/manifest.json b/homeassistant/components/probe_plus/manifest.json index 1d3bd549761bd9..01a0d94b15385a 100644 --- a/homeassistant/components/probe_plus/manifest.json +++ b/homeassistant/components/probe_plus/manifest.json @@ -15,5 +15,5 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["pyprobeplus==1.1.0"] + "requirements": ["pyprobeplus==1.1.1"] } diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 8cf5f8b2b5843e..db82251f5bbc85 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -134,7 +134,9 @@ async def _refresh_token() -> str: api = tesla.vehicles.createSigned(vin) else: api = tesla.vehicles.createFleet(vin) - coordinator = TeslaFleetVehicleDataCoordinator(hass, entry, api, product) + coordinator = TeslaFleetVehicleDataCoordinator( + hass, entry, api, product, Scope.VEHICLE_LOCATION in scopes + ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/tesla_fleet/coordinator.py b/homeassistant/components/tesla_fleet/coordinator.py index e3a31a2c0dca00..4ab332c91525a6 100644 --- a/homeassistant/components/tesla_fleet/coordinator.py +++ b/homeassistant/components/tesla_fleet/coordinator.py @@ -39,9 +39,9 @@ VehicleDataEndpoint.CHARGE_STATE, VehicleDataEndpoint.CLIMATE_STATE, VehicleDataEndpoint.DRIVE_STATE, - VehicleDataEndpoint.LOCATION_DATA, VehicleDataEndpoint.VEHICLE_STATE, VehicleDataEndpoint.VEHICLE_CONFIG, + VehicleDataEndpoint.LOCATION_DATA, ] @@ -65,6 +65,7 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): updated_once: bool pre2021: bool last_active: datetime + endpoints: list[VehicleDataEndpoint] def __init__( self, @@ -72,6 +73,7 @@ def __init__( config_entry: TeslaFleetConfigEntry, api: VehicleFleet, product: dict, + location: bool, ) -> None: """Initialize TeslaFleet Vehicle Update Coordinator.""" super().__init__( @@ -85,6 +87,11 @@ def __init__( self.data = flatten(product) self.updated_once = False self.last_active = datetime.now() + self.endpoints = ( + ENDPOINTS + if location + else [ep for ep in ENDPOINTS if ep != VehicleDataEndpoint.LOCATION_DATA] + ) async def _async_update_data(self) -> dict[str, Any]: """Update vehicle data using TeslaFleet API.""" @@ -97,7 +104,7 @@ async def _async_update_data(self) -> dict[str, Any]: if self.data["state"] != TeslaFleetState.ONLINE: return self.data - response = await self.api.vehicle_data(endpoints=ENDPOINTS) + response = await self.api.vehicle_data(endpoints=self.endpoints) data = response["response"] except VehicleOffline: diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 5b633a12212634..a6409238a161ed 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1940,7 +1940,7 @@ }, "feedreader": { "name": "Feedreader", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling" }, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c51e6a589c64b7..2e1b268888898e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.5.0 aiohasupervisor==0.3.3 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 -aiohttp==3.13.0 +aiohttp==3.13.1 aiohttp_cors==0.8.1 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 68eff5063f6ea9..38408172d16435 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.3", - "aiohttp==3.13.0", + "aiohttp==3.13.1", "aiohttp_cors==0.8.1", "aiohttp-fast-zlib==0.3.0", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index cea224937bfc11..d7e74b4edc1af8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.5.0 aiohasupervisor==0.3.3 -aiohttp==3.13.0 +aiohttp==3.13.1 aiohttp_cors==0.8.1 aiohttp-fast-zlib==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 3782c2adc79b28..55ceebaa41f27b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -10,7 +10,7 @@ AEMET-OpenData==0.6.4 AIOSomecomfort==0.0.33 # homeassistant.components.adax -Adax-local==0.1.5 +Adax-local==0.2.0 # homeassistant.components.doorbird DoorBirdPy==3.0.8 @@ -1328,7 +1328,7 @@ kiwiki-client==0.1.1 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.10.9.185845 +knx-frontend==2025.10.17.202411 # homeassistant.components.konnected konnected==1.2.0 @@ -2305,7 +2305,7 @@ pypoint==3.0.0 pyportainer==1.0.3 # homeassistant.components.probe_plus -pyprobeplus==1.1.0 +pyprobeplus==1.1.1 # homeassistant.components.profiler pyprof2calltree==1.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da3c2a9fbaf821..b969066b7c7694 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ AEMET-OpenData==0.6.4 AIOSomecomfort==0.0.33 # homeassistant.components.adax -Adax-local==0.1.5 +Adax-local==0.2.0 # homeassistant.components.doorbird DoorBirdPy==3.0.8 @@ -1153,7 +1153,7 @@ kegtron-ble==1.0.2 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.10.9.185845 +knx-frontend==2025.10.17.202411 # homeassistant.components.konnected konnected==1.2.0 @@ -1932,7 +1932,7 @@ pypoint==3.0.0 pyportainer==1.0.3 # homeassistant.components.probe_plus -pyprobeplus==1.1.0 +pyprobeplus==1.1.1 # homeassistant.components.profiler pyprof2calltree==1.4.5 diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index b7f451945f4cf0..a2bb7d7e728685 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -2704,6 +2704,59 @@ def mock_async_get(flow_id: str): assert result["next_flow"] == (config_entries.FlowType.CONFIG_FLOW, zwave_flow_id) +@pytest.mark.usefixtures("mock_setup_entry", "mock_zeroconf") +async def test_user_flow_no_zwave_discovery_without_home_id( + hass: HomeAssistant, mock_client: APIClient +) -> None: + """Test that the user flow does not start Z-Wave JS discovery when zwave_home_id is not set.""" + # Mock device with Z-Wave capabilities but no home ID + mock_client.device_info = AsyncMock( + return_value=DeviceInfo( + uses_password=False, + name="test-zwave-device-no-id", + mac_address="11:22:33:44:55:CC", + zwave_proxy_feature_flags=1, + zwave_home_id=0, # No home ID set (fresh adapter or unplugged) + ) + ) + mock_client.connected_address = "192.168.1.103" + + # Track flow.async_init calls + original_async_init = hass.config_entries.flow.async_init + flow_init_calls = [] + + async def track_async_init(*args, **kwargs): + flow_init_calls.append((args, kwargs)) + return await original_async_init(*args, **kwargs) + + with patch.object( + hass.config_entries.flow, "async_init", side_effect=track_async_init + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "192.168.1.103", CONF_PORT: 6053}, + ) + + # Verify the ESPHome entry was created + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test-zwave-device-no-id" + assert result["data"] == { + CONF_HOST: "192.168.1.103", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: "", + CONF_DEVICE_NAME: "test-zwave-device-no-id", + } + + # Verify only ESPHome flow was initiated, no Z-Wave flow + assert len(flow_init_calls) == 1 + assert flow_init_calls[0][0][0] == DOMAIN + + # Verify next_flow was NOT set + assert "next_flow" not in result + + @pytest.mark.usefixtures("mock_setup_entry", "mock_zeroconf") async def test_user_flow_no_zwave_discovery_without_capabilities( hass: HomeAssistant, mock_client: APIClient diff --git a/tests/components/esphome/test_entry_data.py b/tests/components/esphome/test_entry_data.py index a80c77eb5b2bce..c4ff33d316ef2b 100644 --- a/tests/components/esphome/test_entry_data.py +++ b/tests/components/esphome/test_entry_data.py @@ -120,3 +120,35 @@ async def test_discover_zwave() -> None: version=1, ), ) + + +async def test_discover_zwave_without_home_id() -> None: + """Test ESPHome does not start Z-Wave discovery without home ID.""" + hass = Mock() + entry_data = RuntimeEntryData( + "mock-id", + "mock-title", + Mock( + connected_address="mock-client-address", + port=1234, + noise_psk=None, + ), + None, + ) + device_info = Mock( + mac_address="mock-device-info-mac", + zwave_proxy_feature_flags=1, + zwave_home_id=0, # No home ID (fresh adapter or unplugged) + ) + device_info.name = "mock-device-infoname" + + with patch( + "homeassistant.helpers.discovery_flow.async_create_flow" + ) as mock_create_flow: + entry_data.async_on_connect( + hass, + device_info, + None, + ) + # Verify async_create_flow was NOT called when zwave_home_id is 0 + mock_create_flow.assert_not_called() diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 319d70b4e42652..43bf531f378b61 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -21,6 +21,8 @@ UserService, UserServiceArg, UserServiceArgType, + ZWaveProxyRequest, + ZWaveProxyRequestType, ) import pytest @@ -2378,3 +2380,98 @@ async def test_manager_handle_dynamic_encryption_key_connection_error( # Verify key was NOT stored due to connection error assert mac_address not in hass_storage[ENCRYPTION_KEY_STORAGE_KEY]["data"]["keys"] + + +async def test_zwave_proxy_request_home_id_change( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test Z-Wave proxy request handler with HOME_ID_CHANGE request.""" + + device_info = { + "name": "test-zwave-proxy", + "mac_address": "11:22:33:44:55:AA", + "zwave_proxy_feature_flags": 1, + } + + await mock_esphome_device( + mock_client=mock_client, + device_info=device_info, + ) + await hass.async_block_till_done() + + # Get the manager's _async_zwave_proxy_request callback + # It's registered via subscribe_zwave_proxy_request + zwave_proxy_callback = None + for call_item in mock_client.subscribe_zwave_proxy_request.call_args_list: + if call_item[0]: + zwave_proxy_callback = call_item[0][0] + break + + assert zwave_proxy_callback is not None + + # Create a mock request with a different type (not HOME_ID_CHANGE) + # Assuming there are other types, we'll use a placeholder value + request = ZWaveProxyRequest( + type=0, # Not HOME_ID_CHANGE + data=b"\x00\x00\x00\x01", + ) + + # Track flow creation + with patch( + "homeassistant.helpers.discovery_flow.async_create_flow" + ) as mock_create_flow: + # Call the callback + zwave_proxy_callback(request) + await hass.async_block_till_done() + + # Verify no flow was created for non-HOME_ID_CHANGE requests + mock_create_flow.assert_not_called() + + # Create a mock request with HOME_ID_CHANGE type and zwave_home_id as bytes + zwave_home_id = 1234567890 + request = ZWaveProxyRequest( + type=ZWaveProxyRequestType.HOME_ID_CHANGE, + data=zwave_home_id.to_bytes(4, byteorder="big") + + b"\x00\x00", # Extra bytes should be ignored + ) + + # Track flow creation + with patch( + "homeassistant.helpers.discovery_flow.async_create_flow" + ) as mock_create_flow: + # Call the callback + zwave_proxy_callback(request) + await hass.async_block_till_done() + + # Verify async_create_zwave_js_flow was called with correct arguments + mock_create_flow.assert_called_once() + call_args = mock_create_flow.call_args + assert call_args[0][0] == hass + assert call_args[0][1] == "zwave_js" + + +async def test_no_zwave_proxy_subscribe_without_feature_flags( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test Z-Wave proxy request subscription is not registered without feature flags.""" + device_info = { + "name": "test-device", + "mac_address": "11:22:33:44:55:AA", + "zwave_proxy_feature_flags": 0, # No Z-Wave proxy features + } + + # Mock the subscribe_zwave_proxy_request method + mock_client.subscribe_zwave_proxy_request = Mock(return_value=lambda: None) + + await mock_esphome_device( + mock_client=mock_client, + device_info=device_info, + ) + await hass.async_block_till_done() + + # Verify subscribe_zwave_proxy_request was NOT called + mock_client.subscribe_zwave_proxy_request.assert_not_called() diff --git a/tests/components/fritz/test_services.py b/tests/components/fritz/test_services.py index d7b85cbc448004..baec257cbe11b2 100644 --- a/tests/components/fritz/test_services.py +++ b/tests/components/fritz/test_services.py @@ -2,11 +2,19 @@ from unittest.mock import patch -from fritzconnection.core.exceptions import FritzConnectionException, FritzServiceError +from fritzconnection.core.exceptions import ( + FritzActionFailedError, + FritzConnectionException, + FritzServiceError, +) import pytest +from voluptuous import MultipleInvalid from homeassistant.components.fritz.const import DOMAIN -from homeassistant.components.fritz.services import SERVICE_SET_GUEST_WIFI_PW +from homeassistant.components.fritz.services import ( + SERVICE_DIAL, + SERVICE_SET_GUEST_WIFI_PW, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -24,6 +32,7 @@ async def test_setup_services(hass: HomeAssistant) -> None: services = hass.services.async_services_for_domain(DOMAIN) assert services assert SERVICE_SET_GUEST_WIFI_PW in services + assert SERVICE_DIAL in services async def test_service_set_guest_wifi_password( @@ -132,3 +141,205 @@ async def test_service_set_guest_wifi_password_unloaded( 'ServiceValidationError: Failed to perform action "set_guest_wifi_password". Config entry for target not found' in caplog.text ) + + +async def test_service_dial( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + caplog: pytest.LogCaptureFixture, + fc_class_mock, + fh_class_mock, +) -> None: + """Test service dial.""" + assert await async_setup_component(hass, DOMAIN, {}) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + device = device_registry.async_get_device( + identifiers={(DOMAIN, "1C:ED:6F:12:34:11")} + ) + assert device + with patch( + "homeassistant.components.fritz.coordinator.AvmWrapper.async_trigger_dial" + ) as mock_async_trigger_dial: + await hass.services.async_call( + DOMAIN, + SERVICE_DIAL, + {"device_id": device.id, "number": "1234567890", "max_ring_seconds": 10}, + ) + assert mock_async_trigger_dial.called + assert mock_async_trigger_dial.call_args.kwargs == {"max_ring_seconds": 10} + assert mock_async_trigger_dial.call_args.args == ("1234567890",) + + +async def test_service_dial_unknown_parameter( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + caplog: pytest.LogCaptureFixture, + fc_class_mock, + fh_class_mock, +) -> None: + """Test service dial with unknown parameters.""" + assert await async_setup_component(hass, DOMAIN, {}) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + device = device_registry.async_get_device( + identifiers={(DOMAIN, "1C:ED:6F:12:34:11")} + ) + assert device + + with patch( + "homeassistant.components.fritz.coordinator.AvmWrapper.async_trigger_dial", + side_effect=FritzServiceError("boom"), + ) as mock_async_trigger_dial: + await hass.services.async_call( + DOMAIN, + SERVICE_DIAL, + {"device_id": device.id, "number": "1234567890", "max_ring_seconds": 10}, + ) + assert mock_async_trigger_dial.called + assert "HomeAssistantError: Action or parameter unknown" in caplog.text + + +async def test_service_dial_wrong_parameter( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + caplog: pytest.LogCaptureFixture, + fc_class_mock, + fh_class_mock, +) -> None: + """Test service dial with unknown parameters.""" + assert await async_setup_component(hass, DOMAIN, {}) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + device = device_registry.async_get_device( + identifiers={(DOMAIN, "1C:ED:6F:12:34:11")} + ) + assert device + + with patch( + "homeassistant.components.fritz.coordinator.AvmWrapper.async_trigger_dial", + ) as mock_async_trigger_dial: + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_DIAL, + { + "device_id": device.id, + "number": "1234567890", + "max_ring_seconds": "", + }, + ) + assert not mock_async_trigger_dial.called + with patch( + "homeassistant.components.fritz.coordinator.AvmWrapper.async_trigger_dial", + ) as mock_async_trigger_dial: + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_DIAL, + { + "device_id": device.id, + "number": "1234567890", + "max_ring_seconds": 0, + }, + ) + assert not mock_async_trigger_dial.called + + +async def test_service_dial_service_not_supported( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + caplog: pytest.LogCaptureFixture, + fc_class_mock, + fh_class_mock, +) -> None: + """Test service dial with connection error.""" + assert await async_setup_component(hass, DOMAIN, {}) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + device = device_registry.async_get_device( + identifiers={(DOMAIN, "1C:ED:6F:12:34:11")} + ) + assert device + + with patch( + "homeassistant.components.fritz.coordinator.AvmWrapper.async_trigger_dial", + side_effect=FritzConnectionException("boom"), + ) as mock_async_trigger_dial: + await hass.services.async_call( + DOMAIN, + SERVICE_DIAL, + {"device_id": device.id, "number": "1234567890", "max_ring_seconds": 10}, + ) + assert mock_async_trigger_dial.called + assert "HomeAssistantError: Action not supported" in caplog.text + + +async def test_service_dial_failed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + caplog: pytest.LogCaptureFixture, + fc_class_mock, + fh_class_mock, +) -> None: + """Test dial service when the dial help is disabled.""" + assert await async_setup_component(hass, DOMAIN, {}) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + device = device_registry.async_get_device( + identifiers={(DOMAIN, "1C:ED:6F:12:34:11")} + ) + assert device + + with patch( + "homeassistant.components.fritz.coordinator.AvmWrapper.async_trigger_dial", + side_effect=FritzActionFailedError("boom"), + ) as mock_async_trigger_dial: + await hass.services.async_call( + DOMAIN, + SERVICE_DIAL, + {"device_id": device.id, "number": "1234567890", "max_ring_seconds": 10}, + ) + assert mock_async_trigger_dial.called + assert ( + "HomeAssistantError: Failed to dial, check if the click to dial service of the FRITZ!Box is activated" + in caplog.text + ) + + +async def test_service_dial_unloaded( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test service dial.""" + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.fritz.coordinator.AvmWrapper.async_trigger_dial" + ) as mock_async_trigger_dial: + await hass.services.async_call( + DOMAIN, + SERVICE_DIAL, + {"device_id": "12345678", "number": "1234567890", "max_ring_seconds": 10}, + ) + assert not mock_async_trigger_dial.called + assert ( + f'ServiceValidationError: Failed to perform action "{SERVICE_DIAL}". Config entry for target not found' + in caplog.text + ) diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 8ed59bc78d1d0e..98dda1c8fe979e 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -508,8 +508,14 @@ async def test_no_follow_logs_compress( hassio_client: TestClient, aioclient_mock: AiohttpClientMocker ) -> None: """Test that we do not compress follow logs.""" - aioclient_mock.get("http://127.0.0.1/supervisor/logs/follow") - aioclient_mock.get("http://127.0.0.1/supervisor/logs") + aioclient_mock.get( + "http://127.0.0.1/supervisor/logs/follow", + headers={"Content-Type": "text/plain"}, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/logs", + headers={"Content-Type": "text/plain"}, + ) resp1 = await hassio_client.get("/api/hassio/supervisor/logs/follow") resp2 = await hassio_client.get("/api/hassio/supervisor/logs") diff --git a/tests/components/knx/snapshots/test_websocket.ambr b/tests/components/knx/snapshots/test_websocket.ambr index 388c68e0d3f4f0..69a329151b91b4 100644 --- a/tests/components/knx/snapshots/test_websocket.ambr +++ b/tests/components/knx/snapshots/test_websocket.ambr @@ -3,12 +3,6 @@ dict({ 'id': 1, 'result': list([ - dict({ - 'collapsible': False, - 'name': 'section_binary_sensor', - 'required': False, - 'type': 'knx_section_flat', - }), dict({ 'name': 'ga_sensor', 'options': dict({ @@ -99,12 +93,6 @@ dict({ 'id': 1, 'result': list([ - dict({ - 'collapsible': False, - 'name': 'section_binary_control', - 'required': False, - 'type': 'knx_section_flat', - }), dict({ 'name': 'ga_up_down', 'optional': True, @@ -134,12 +122,6 @@ }), 'type': 'ha_selector', }), - dict({ - 'collapsible': False, - 'name': 'section_stop_control', - 'required': False, - 'type': 'knx_section_flat', - }), dict({ 'name': 'ga_stop', 'optional': True, @@ -322,12 +304,6 @@ dict({ 'id': 1, 'result': list([ - dict({ - 'collapsible': False, - 'name': 'section_switch', - 'required': False, - 'type': 'knx_section_flat', - }), dict({ 'name': 'ga_switch', 'optional': True, @@ -349,12 +325,6 @@ 'required': False, 'type': 'knx_group_address', }), - dict({ - 'collapsible': False, - 'name': 'section_brightness', - 'required': False, - 'type': 'knx_section_flat', - }), dict({ 'name': 'ga_brightness', 'optional': True, @@ -508,12 +478,6 @@ }), dict({ 'schema': list([ - dict({ - 'collapsible': False, - 'name': 'section_red', - 'required': False, - 'type': 'knx_section_flat', - }), dict({ 'name': 'ga_red_switch', 'optional': True, @@ -555,12 +519,6 @@ 'required': True, 'type': 'knx_group_address', }), - dict({ - 'collapsible': False, - 'name': 'section_green', - 'required': False, - 'type': 'knx_section_flat', - }), dict({ 'name': 'ga_green_switch', 'optional': True, @@ -602,12 +560,6 @@ 'required': True, 'type': 'knx_group_address', }), - dict({ - 'collapsible': False, - 'name': 'section_blue', - 'required': False, - 'type': 'knx_section_flat', - }), dict({ 'name': 'ga_blue_switch', 'optional': True, @@ -649,12 +601,6 @@ 'required': True, 'type': 'knx_group_address', }), - dict({ - 'collapsible': False, - 'name': 'section_white', - 'required': False, - 'type': 'knx_section_flat', - }), dict({ 'name': 'ga_white_switch', 'optional': True, @@ -767,12 +713,6 @@ dict({ 'id': 1, 'result': list([ - dict({ - 'collapsible': False, - 'name': 'section_switch', - 'required': False, - 'type': 'knx_section_flat', - }), dict({ 'name': 'ga_switch', 'options': dict({ diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index e5b9f0fc467799..e2df2597345476 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -98,6 +98,7 @@ async def integration_fixture( "flow_sensor", "generic_switch", "generic_switch_multi", + "haijai_switch", "humidity_sensor", "inovelli_vtm30", "laundry_dryer", diff --git a/tests/components/matter/fixtures/nodes/haijai_switch.json b/tests/components/matter/fixtures/nodes/haijai_switch.json new file mode 100644 index 00000000000000..78dca88c1ae33d --- /dev/null +++ b/tests/components/matter/fixtures/nodes/haijai_switch.json @@ -0,0 +1,581 @@ +{ + "node_id": 3, + "date_commissioned": "2025-06-23T07:17:49.888306", + "last_interview": "2025-06-23T07:17:49.888640", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 2 + } + ], + "0/29/1": [ + 29, 31, 40, 42, 43, 47, 48, 49, 50, 51, 52, 53, 54, 60, 62, 63, 70 + ], + "0/29/2": [41], + "0/29/3": [1, 2, 3, 4, 5, 6], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 3 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 17, + "0/40/1": "HAOJAI", + "0/40/2": 65521, + "0/40/3": "HJMT-6B", + "0/40/4": 32772, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 0, + "0/40/8": "1", + "0/40/9": 34013000, + "0/40/10": "1.0", + "0/40/11": "20200101", + "0/40/15": "CN0H052432054873", + "0/40/18": "704A613A7A600704", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/21": 16973824, + "0/40/22": 1, + "0/40/65532": 0, + "0/40/65533": 3, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 15, 18, 19, 21, 22, 65528, 65529, + 65531, 65532, 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 0, + "0/42/3": 0, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/43/0": "", + "0/43/1": [], + "0/43/65532": 0, + "0/43/65533": 1, + "0/43/65528": [], + "0/43/65529": [], + "0/43/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "0/47/0": 0, + "0/47/1": 0, + "0/47/2": "Primary Battery", + "0/47/11": 2316, + "0/47/12": 2, + "0/47/31": [], + "0/47/65532": 6, + "0/47/65533": 2, + "0/47/65528": [], + "0/47/65529": [], + "0/47/65531": [0, 1, 2, 11, 12, 31, 65528, 65529, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "0": "l7EmEshkQdo=", + "1": true + } + ], + "0/49/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "l7EmEshkQdo=", + "0/49/7": null, + "0/49/8": [0], + "0/49/9": 4, + "0/49/10": 4, + "0/49/65532": 2, + "0/49/65533": 2, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 3, 4, 6, 8], + "0/49/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 65528, 65529, 65531, 65532, 65533 + ], + "0/50/65532": 0, + "0/50/65533": 1, + "0/50/65528": [1], + "0/50/65529": [0], + "0/50/65531": [65528, 65529, 65531, 65532, 65533], + "0/51/0": [ + { + "0": "ieee802154", + "1": true, + "2": null, + "3": null, + "4": "rsJrAP1hSHs=", + "5": [], + "6": [ + "/ZexJhLIAAAO/87mUIm0Aw==", + "/oAAAAAAAACswmsA/WFIew==", + "/ea7QcuSAAAZY91N85Tccg==" + ], + "7": 4 + } + ], + "0/51/1": 1, + "0/51/2": 21582509, + "0/51/3": 5995, + "0/51/4": 6, + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [0, 1, 2, 3, 4, 8, 65528, 65529, 65531, 65532, 65533], + "0/52/1": 7880, + "0/52/2": 188, + "0/52/65532": 0, + "0/52/65533": 1, + "0/52/65528": [], + "0/52/65529": [], + "0/52/65531": [1, 2, 65528, 65529, 65531, 65532, 65533], + "0/53/0": 25, + "0/53/1": 2, + "0/53/2": "MyHome***", + "0/53/3": 34374, + "0/53/4": 123456789, + "0/53/5": "123456789", + "0/53/6": 0, + "0/53/7": [ + { + "0": 7411887479468786724, + "1": 143151, + "2": 50176, + "3": 2193912, + "4": 136760, + "5": 3, + "6": -67, + "7": -68, + "8": 25, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + } + ], + "0/53/8": [ + { + "0": 7411887479468786724, + "1": 50176, + "2": 49, + "3": 0, + "4": 0, + "5": 3, + "6": 3, + "7": 47, + "8": true, + "9": true + } + ], + "0/53/9": 492013191, + "0/53/10": 68, + "0/53/11": 15, + "0/53/12": 24, + "0/53/13": 26, + "0/53/14": 711, + "0/53/15": 711, + "0/53/16": 0, + "0/53/17": 0, + "0/53/18": 3643, + "0/53/19": 54, + "0/53/20": 0, + "0/53/21": 455, + "0/53/22": 22757436, + "0/53/23": 22713802, + "0/53/24": 43634, + "0/53/25": 22713802, + "0/53/26": 22617743, + "0/53/27": 43634, + "0/53/28": 783991, + "0/53/29": 21973603, + "0/53/30": 0, + "0/53/31": 0, + "0/53/32": 0, + "0/53/33": 18859857, + "0/53/34": 18078, + "0/53/35": 0, + "0/53/36": 6, + "0/53/37": 0, + "0/53/38": 0, + "0/53/39": 6266185, + "0/53/40": 1482630, + "0/53/41": 20602, + "0/53/42": 1258613, + "0/53/43": 0, + "0/53/44": 0, + "0/53/45": 0, + "0/53/46": 0, + "0/53/47": 0, + "0/53/48": 4569849, + "0/53/49": 239174, + "0/53/50": 61669, + "0/53/51": 11739, + "0/53/52": 0, + "0/53/53": 5445, + "0/53/54": 98846, + "0/53/55": 20850, + "0/53/56": 0, + "0/53/57": 0, + "0/53/58": 0, + "0/53/59": { + "0": 672, + "1": 8335 + }, + "0/53/60": "AB//wA==", + "0/53/61": { + "0": true, + "1": false, + "2": true, + "3": true, + "4": true, + "5": true, + "6": false, + "7": true, + "8": true, + "9": true, + "10": true, + "11": true + }, + "0/53/62": [], + "0/53/65532": 15, + "0/53/65533": 2, + "0/53/65528": [], + "0/53/65529": [0], + "0/53/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 65528, 65529, 65531, 65532, 65533 + ], + "0/54/0": null, + "0/54/1": null, + "0/54/2": null, + "0/54/3": null, + "0/54/4": null, + "0/54/6": null, + "0/54/7": null, + "0/54/8": null, + "0/54/9": null, + "0/54/10": null, + "0/54/65532": 1, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [], + "0/54/65531": [ + 0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 65528, 65529, 65531, 65532, 65533 + ], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 1, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRAxgkBwEkCAEwCUEETqaWCJhMRenkRB9o8nXn0BvopCBn7Umyk5eb/4h82I8AjcYYvRj2dy2uI2c8vJwfAd9n1BgakpoLyaDINvnkqzcKNQEoARgkAgE2AwQCBAEYMAQUP2cjEYlhgO9Vdv99GqNRaEsN2EQwBRQlbzhp1Z5Rjw5z2pXAYP5w/poPfBgwC0Ds4Do5QpvcESDwt5irKuVO8xdvIdcsu6NdaG0j2cJS15FEnRFu3r3JbdL8hgp98zOey44e7fIM/UY1XwcPmXiDGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEENDoJtanVVtulgCvH+qZmPAgr+Nx9mFNTbLUrKiIw0aj2eX5sYFeERDoY/VzTZmFXKfytdHn3vuqjw3ns/fsE6TcKNQEpARgkAmAwBBQlbzhp1Z5Rjw5z2pXAYP5w/poPfDAFFM1Zf9RugnSEHoYL0LWBRgK2mAXBGDALQPaAK9lJmzITsU722xwS38xoyYYBPiI2+TiXrp4odN/DuFZIc0ORD9dpsphFfUXnmVd/NLmfK+a4xk4H0OT0racY", + "254": 3 + } + ], + "0/62/1": [ + { + "1": "BHGIXf0GdtbHONKYkc3TlP2n0VuBvbHWRJws3psD6TS6hHOd9w+ZCn3DfukD7+7ds3JAZ5lACACw8arie7bbqcg=", + "2": 4939, + "3": 2, + "4": 3, + "5": "Home", + "254": 3 + } + ], + "0/62/2": 5, + "0/62/3": 3, + "0/62/4": [ + "FTABAQAkAgE3AyYUUYKBASYVVxTP5BgmBK1VUiwkBQA3BiYUUYKBASYVVxTP5BgkBwEkCAEwCUEEdTVneq5d3MMxN7Mu3oGXBPm8tXyLmLTSo3DRfE6PCsKnhSbKtwF+8z2reRmo5vH6+ueuxHXMbIrVGfHj0j2CwTcKNQEpARgkAmAwBBR7GKDn5Hvvijcq0AEWuWJVJiSeXTAFFHsYoOfke++KNyrQARa5YlUmJJ5dGDALQOZF1wamQXpLq0HFUUzrJUit14mqphqnA2UfKFTPniY8md63dKBl1yaHE4MQb8ugtg/vQK8HzmjSPa3H8YtBYdcY", + "FTABAQAkAgE3AycUkHhx53rJ5DUmFaPLDygYJgS/vFUuJAUANwYnFJB4ced6yeQ1JhWjyw8oGCQHASQIATAJQQQ9wuwuiuxSe0I2dSj71nJImjzbTmoTNVZQpoJpdSS0jFpSAHl1IOdXpP+XoAKynUelx4gFmKcW0P7Txertp9EUNwo1ASkBGCQCYDAEFDcI25Os+n2EfgW6BgGvO0GiL0sgMAUUNwjbk6z6fYR+BboGAa87QaIvSyAYMAtAKLE9XM+LHMmuLmnNNE1eZJeHIWDCv4/mUyFE9SYB7CKx6F8/2gWiR3+Iue20GAPVumLcTadsNpcCkWMgj++Skhg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEcYhd/QZ21sc40piRzdOU/afRW4G9sdZEnCzemwPpNLqEc533D5kKfcN+6QPv7t2zckBnmUAIALDxquJ7ttupyDcKNQEpARgkAmAwBBTNWX/UboJ0hB6GC9C1gUYCtpgFwTAFFM1Zf9RugnSEHoYL0LWBRgK2mAXBGDALQJKKLHFq59hWFvFf4bv6vjqzN2AC0f1vvASOcIaZ2PKWtzB4fyCsVHYkrAlbpGzkLjuBRa6wXk19xDy/5XLbjQ0Y" + ], + "0/62/5": 3, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 2, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/70/0": 120, + "0/70/1": 300, + "0/70/2": 300, + "0/70/65532": 0, + "0/70/65533": 2, + "0/70/65528": [], + "0/70/65529": [], + "0/70/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 2, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "0": 15, + "1": 1 + } + ], + "1/29/1": [3, 29, 30, 59], + "1/29/2": [], + "1/29/3": [], + "1/29/4": [ + { + "0": null, + "1": 7, + "2": 1 + } + ], + "1/29/65532": 1, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "1/30/0": [], + "1/30/65532": 0, + "1/30/65533": 1, + "1/30/65528": [], + "1/30/65529": [], + "1/30/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/59/0": 2, + "1/59/1": 0, + "1/59/65532": 30, + "1/59/65533": 1, + "1/59/65528": [], + "1/59/65529": [], + "1/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "2/3/0": 0, + "2/3/1": 0, + "2/3/65532": 0, + "2/3/65533": 4, + "2/3/65528": [], + "2/3/65529": [0, 64], + "2/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "2/29/0": [ + { + "0": 15, + "1": 1 + } + ], + "2/29/1": [3, 29, 59], + "2/29/2": [], + "2/29/3": [], + "2/29/4": [ + { + "0": null, + "1": 7, + "2": 2 + } + ], + "2/29/65532": 1, + "2/29/65533": 2, + "2/29/65528": [], + "2/29/65529": [], + "2/29/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "2/59/0": 2, + "2/59/1": 0, + "2/59/65532": 30, + "2/59/65533": 1, + "2/59/65528": [], + "2/59/65529": [], + "2/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "3/3/0": 0, + "3/3/1": 0, + "3/3/65532": 0, + "3/3/65533": 4, + "3/3/65528": [], + "3/3/65529": [0, 64], + "3/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "3/29/0": [ + { + "0": 15, + "1": 1 + } + ], + "3/29/1": [3, 29, 59], + "3/29/2": [], + "3/29/3": [], + "3/29/4": [ + { + "0": null, + "1": 7, + "2": 3 + } + ], + "3/29/65532": 1, + "3/29/65533": 2, + "3/29/65528": [], + "3/29/65529": [], + "3/29/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "3/59/0": 2, + "3/59/1": 0, + "3/59/65532": 30, + "3/59/65533": 1, + "3/59/65528": [], + "3/59/65529": [], + "3/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "4/3/0": 0, + "4/3/1": 0, + "4/3/65532": 0, + "4/3/65533": 4, + "4/3/65528": [], + "4/3/65529": [0, 64], + "4/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "4/29/0": [ + { + "0": 15, + "1": 1 + } + ], + "4/29/1": [3, 29, 59], + "4/29/2": [], + "4/29/3": [], + "4/29/4": [ + { + "0": null, + "1": 7, + "2": 4 + } + ], + "4/29/65532": 1, + "4/29/65533": 2, + "4/29/65528": [], + "4/29/65529": [], + "4/29/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "4/59/0": 2, + "4/59/1": 0, + "4/59/65532": 30, + "4/59/65533": 1, + "4/59/65528": [], + "4/59/65529": [], + "4/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "5/3/0": 0, + "5/3/1": 0, + "5/3/65532": 0, + "5/3/65533": 4, + "5/3/65528": [], + "5/3/65529": [0, 64], + "5/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "5/29/0": [ + { + "0": 15, + "1": 1 + } + ], + "5/29/1": [3, 29, 59], + "5/29/2": [], + "5/29/3": [], + "5/29/4": [ + { + "0": null, + "1": 7, + "2": 5 + } + ], + "5/29/65532": 1, + "5/29/65533": 2, + "5/29/65528": [], + "5/29/65529": [], + "5/29/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "5/59/0": 2, + "5/59/1": 0, + "5/59/65532": 30, + "5/59/65533": 1, + "5/59/65528": [], + "5/59/65529": [], + "5/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "6/3/0": 0, + "6/3/1": 0, + "6/3/65532": 0, + "6/3/65533": 4, + "6/3/65528": [], + "6/3/65529": [0, 64], + "6/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "6/29/0": [ + { + "0": 15, + "1": 1 + } + ], + "6/29/1": [3, 29, 59], + "6/29/2": [], + "6/29/3": [], + "6/29/4": [ + { + "0": null, + "1": 7, + "2": 6 + } + ], + "6/29/65532": 1, + "6/29/65533": 2, + "6/29/65528": [], + "6/29/65529": [], + "6/29/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "6/59/0": 2, + "6/59/1": 0, + "6/59/65532": 30, + "6/59/65533": 1, + "6/59/65528": [], + "6/59/65529": [], + "6/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_button.ambr b/tests/components/matter/snapshots/test_button.ambr index 4765a089f0515e..f0bc0a134f332a 100644 --- a/tests/components/matter/snapshots/test_button.ambr +++ b/tests/components/matter/snapshots/test_button.ambr @@ -975,6 +975,55 @@ 'state': 'unknown', }) # --- +# name: test_buttons[haijai_switch][button.hjmt_6b_identify_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.hjmt_6b_identify_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Identify (1)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-IdentifyButton-3-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[haijai_switch][button.hjmt_6b_identify_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'identify', + 'friendly_name': 'HJMT-6B Identify (1)', + }), + 'context': , + 'entity_id': 'button.hjmt_6b_identify_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_buttons[inovelli_vtm30][button.white_series_onoff_switch_load_control-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_event.ambr b/tests/components/matter/snapshots/test_event.ambr index f760e3fb375ea4..fbde547f701254 100644 --- a/tests/components/matter/snapshots/test_event.ambr +++ b/tests/components/matter/snapshots/test_event.ambr @@ -192,6 +192,384 @@ 'state': 'unknown', }) # --- +# name: test_events[haijai_switch][event.hjmt_6b_button_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.hjmt_6b_button_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Button (1)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'button', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-GenericSwitch-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[haijai_switch][event.hjmt_6b_button_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'button', + 'event_type': None, + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + 'friendly_name': 'HJMT-6B Button (1)', + }), + 'context': , + 'entity_id': 'event.hjmt_6b_button_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_events[haijai_switch][event.hjmt_6b_button_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.hjmt_6b_button_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Button (2)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'button', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-2-GenericSwitch-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[haijai_switch][event.hjmt_6b_button_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'button', + 'event_type': None, + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + 'friendly_name': 'HJMT-6B Button (2)', + }), + 'context': , + 'entity_id': 'event.hjmt_6b_button_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_events[haijai_switch][event.hjmt_6b_button_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.hjmt_6b_button_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Button (3)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'button', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-3-GenericSwitch-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[haijai_switch][event.hjmt_6b_button_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'button', + 'event_type': None, + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + 'friendly_name': 'HJMT-6B Button (3)', + }), + 'context': , + 'entity_id': 'event.hjmt_6b_button_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_events[haijai_switch][event.hjmt_6b_button_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.hjmt_6b_button_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Button (4)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'button', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-4-GenericSwitch-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[haijai_switch][event.hjmt_6b_button_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'button', + 'event_type': None, + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + 'friendly_name': 'HJMT-6B Button (4)', + }), + 'context': , + 'entity_id': 'event.hjmt_6b_button_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_events[haijai_switch][event.hjmt_6b_button_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.hjmt_6b_button_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Button (5)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'button', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-5-GenericSwitch-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[haijai_switch][event.hjmt_6b_button_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'button', + 'event_type': None, + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + 'friendly_name': 'HJMT-6B Button (5)', + }), + 'context': , + 'entity_id': 'event.hjmt_6b_button_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_events[haijai_switch][event.hjmt_6b_button_6-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.hjmt_6b_button_6', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Button (6)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'button', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-6-GenericSwitch-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[haijai_switch][event.hjmt_6b_button_6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'button', + 'event_type': None, + 'event_types': list([ + 'multi_press_1', + 'multi_press_2', + 'long_press', + 'long_release', + ]), + 'friendly_name': 'HJMT-6B Button (6)', + }), + 'context': , + 'entity_id': 'event.hjmt_6b_button_6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_events[inovelli_vtm30][event.white_series_onoff_switch_config-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 247f01cc96b4c0..e11c0f03bf13b4 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -3984,6 +3984,424 @@ 'state': '0', }) # --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_battery-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.hjmt_6b_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': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-0-PowerSource-47-12', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'HJMT-6B Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hjmt_6b_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_battery_voltage-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.hjmt_6b_battery_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery voltage', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_voltage', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-0-PowerSourceBatVoltage-47-11', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_battery_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'HJMT-6B Battery voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hjmt_6b_battery_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.316', + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_1-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.hjmt_6b_current_switch_position_1', + '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': 'Current switch position (1)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HJMT-6B Current switch position (1)', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.hjmt_6b_current_switch_position_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_2-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.hjmt_6b_current_switch_position_2', + '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': 'Current switch position (2)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-2-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HJMT-6B Current switch position (2)', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.hjmt_6b_current_switch_position_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_3-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.hjmt_6b_current_switch_position_3', + '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': 'Current switch position (3)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-3-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HJMT-6B Current switch position (3)', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.hjmt_6b_current_switch_position_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_4-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.hjmt_6b_current_switch_position_4', + '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': 'Current switch position (4)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-4-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HJMT-6B Current switch position (4)', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.hjmt_6b_current_switch_position_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_5-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.hjmt_6b_current_switch_position_5', + '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': 'Current switch position (5)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-5-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HJMT-6B Current switch position (5)', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.hjmt_6b_current_switch_position_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_6-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.hjmt_6b_current_switch_position_6', + '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': 'Current switch position (6)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-6-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[haijai_switch][sensor.hjmt_6b_current_switch_position_6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HJMT-6B Current switch position (6)', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.hjmt_6b_current_switch_position_6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensors[humidity_sensor][sensor.mock_humidity_sensor_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/openrgb/test_init.py b/tests/components/openrgb/test_init.py index 55e2c8c616f655..9d1637da811d81 100644 --- a/tests/components/openrgb/test_init.py +++ b/tests/components/openrgb/test_init.py @@ -8,6 +8,7 @@ import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.openrgb import async_remove_config_entry_device from homeassistant.components.openrgb.const import DOMAIN, SCAN_INTERVAL from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_ON, STATE_UNAVAILABLE @@ -59,6 +60,132 @@ async def test_server_device_registry( assert server_device == snapshot +async def test_remove_config_entry_device_server( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_openrgb_client: MagicMock, +) -> None: + """Test that server device cannot be removed.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + device_registry = dr.async_get(hass) + server_device = device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.entry_id)} + ) + + assert server_device is not None + + # Try to remove server device - should be blocked + result = await async_remove_config_entry_device( + hass, mock_config_entry, server_device + ) + + assert result is False + + +async def test_remove_config_entry_device_still_connected( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_openrgb_client: MagicMock, +) -> None: + """Test that connected devices cannot be removed.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + device_registry = dr.async_get(hass) + + # Get a device that's in coordinator.data (still connected) + devices = dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id + ) + rgb_device = next( + (d for d in devices if d.identifiers != {(DOMAIN, mock_config_entry.entry_id)}), + None, + ) + + if rgb_device: + # Try to remove device that's still connected - should be blocked + result = await async_remove_config_entry_device( + hass, mock_config_entry, rgb_device + ) + assert result is False + + +async def test_remove_config_entry_device_disconnected( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_openrgb_client: MagicMock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test that disconnected devices can be removed.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Create a device that's not in coordinator.data (disconnected) + entry_id = mock_config_entry.entry_id + disconnected_device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={ + ( + DOMAIN, + f"{entry_id}||KEYBOARD||Old Vendor||Old Device||OLD123||Old Location", + ) + }, + name="Old Disconnected Device", + via_device=(DOMAIN, entry_id), + ) + + # Try to remove disconnected device - should succeed + result = await async_remove_config_entry_device( + hass, mock_config_entry, disconnected_device + ) + + assert result is True + + +@pytest.mark.usefixtures("mock_openrgb_client") +async def test_remove_config_entry_device_with_multiple_identifiers( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device removal with multiple domain identifiers.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entry_id = mock_config_entry.entry_id + + # Create a device with identifiers from multiple domains + device_with_multiple_identifiers = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={ + ("other_domain", "some_other_id"), # This should be skipped + ( + DOMAIN, + f"{entry_id}||DEVICE||Vendor||Name||SERIAL123||Location", + ), # This is a disconnected OpenRGB device + }, + name="Multi-Domain Device", + via_device=(DOMAIN, entry_id), + ) + + # Try to remove device - should succeed because the OpenRGB identifier is disconnected + result = await async_remove_config_entry_device( + hass, mock_config_entry, device_with_multiple_identifiers + ) + + assert result is True + + @pytest.mark.parametrize( ("exception", "expected_state"), [ diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index 3645a0f434da8e..02f1ca940a6741 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -9,6 +9,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion +from tesla_fleet_api.const import Scope, VehicleDataEndpoint from tesla_fleet_api.exceptions import ( InvalidRegion, InvalidToken, @@ -36,6 +37,7 @@ from homeassistant.helpers import device_registry as dr from . import setup_platform +from .conftest import create_config_entry from .const import VEHICLE_ASLEEP, VEHICLE_DATA_ALT from tests.common import MockConfigEntry, async_fire_time_changed @@ -497,3 +499,65 @@ async def test_bad_implementation( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert not result["errors"] + + +async def test_vehicle_without_location_scope( + hass: HomeAssistant, + expires_at: int, + mock_vehicle_data: AsyncMock, +) -> None: + """Test vehicle setup without VEHICLE_LOCATION scope excludes location endpoint.""" + + # Create config entry without VEHICLE_LOCATION scope + config_entry = create_config_entry( + expires_at, + [ + Scope.OPENID, + Scope.OFFLINE_ACCESS, + Scope.VEHICLE_DEVICE_DATA, + # Deliberately exclude Scope.VEHICLE_LOCATION + ], + ) + + await setup_platform(hass, config_entry) + assert config_entry.state is ConfigEntryState.LOADED + + # Verify that vehicle_data was called without LOCATION_DATA endpoint + mock_vehicle_data.assert_called() + call_args = mock_vehicle_data.call_args + endpoints = call_args.kwargs.get("endpoints", []) + + # Should not include LOCATION_DATA endpoint + assert VehicleDataEndpoint.LOCATION_DATA not in endpoints + + # Should include other endpoints + assert VehicleDataEndpoint.CHARGE_STATE in endpoints + assert VehicleDataEndpoint.CLIMATE_STATE in endpoints + assert VehicleDataEndpoint.DRIVE_STATE in endpoints + assert VehicleDataEndpoint.VEHICLE_STATE in endpoints + assert VehicleDataEndpoint.VEHICLE_CONFIG in endpoints + + +async def test_vehicle_with_location_scope( + hass: HomeAssistant, + normal_config_entry: MockConfigEntry, + mock_vehicle_data: AsyncMock, +) -> None: + """Test vehicle setup with VEHICLE_LOCATION scope includes location endpoint.""" + await setup_platform(hass, normal_config_entry) + assert normal_config_entry.state is ConfigEntryState.LOADED + + # Verify that vehicle_data was called with LOCATION_DATA endpoint + mock_vehicle_data.assert_called() + call_args = mock_vehicle_data.call_args + endpoints = call_args.kwargs.get("endpoints", []) + + # Should include LOCATION_DATA endpoint when scope is present + assert VehicleDataEndpoint.LOCATION_DATA in endpoints + + # Should include all other endpoints + assert VehicleDataEndpoint.CHARGE_STATE in endpoints + assert VehicleDataEndpoint.CLIMATE_STATE in endpoints + assert VehicleDataEndpoint.DRIVE_STATE in endpoints + assert VehicleDataEndpoint.VEHICLE_STATE in endpoints + assert VehicleDataEndpoint.VEHICLE_CONFIG in endpoints