Compare commits

...

43 Commits

Author SHA1 Message Date
Ludovic BOUÉ
1f682f067a Add mock thermostat entity snapshots for testing 2025-12-01 18:09:42 +00:00
Ludovic BOUÉ
8bf279505c Add test for thermostat remote sensing binary sensors 2025-12-01 17:53:45 +00:00
Ludovic BOUÉ
013200f82e Remove Eve Thermo remote sensing binary sensor snapshots from test 2025-12-01 17:53:09 +00:00
Ludovic BOUÉ
18ec053245 Add mock thermostat fixture to integration tests 2025-12-01 17:52:02 +00:00
Ludovic BOUÉ
3e1897054d Add snapshots for Eve Thermo remote sensing binary sensors 2025-12-01 17:51:33 +00:00
Ludovic BOUÉ
de369fe8b9 Add mock thermostat JSON fixture for testing 2025-12-01 17:51:14 +00:00
Ludovic BOUÉ
29e5e3557e Add remote sensing strings for thermostat features 2025-12-01 17:20:02 +00:00
Ludovic BOUÉ
343eb83f50 ThermostatRemoteSensing 2025-12-01 17:12:04 +00:00
Maciej Bieniek
060ad35ddc Bump aioshelly to version 13.22.0 (#157629) 2025-12-01 15:47:53 +01:00
Erik Montnemery
6c5dba40cd Remove cover triggers (#157621) 2025-12-01 14:09:29 +01:00
Erik Montnemery
a04d595424 Remove description_configured from condition and trigger translations (#157620) 2025-12-01 12:57:07 +01:00
Lukas
fe85eaf2a2 Pooldose: Add sensors for water meter (#157382) 2025-12-01 11:43:50 +01:00
Raphael Hehl
3551c4b01f Fix UniFi Protect G6 Instant speaker volume control (#157549) 2025-12-01 11:38:34 +01:00
Shay Levy
e7edd51a65 Refactor Shelly number platform to use upstream set_thermostat_state (#157527)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-01 12:37:12 +02:00
Raphael Hehl
0c4f2326ef Use public API for UniFi Protect light brightness control (#157550) 2025-12-01 11:32:03 +01:00
dependabot[bot]
81f4456d7c Bump actions/ai-inference from 2.0.3 to 2.0.4 (#157608)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 11:04:04 +01:00
Maciej Bieniek
2b608bf15c Remove name from Shelly RGBCCT sensors (#157492) 2025-12-01 10:54:19 +01:00
karwosts
972ed4b27f Finish removal of sensor.sun_solar_rising (#157606) 2025-12-01 09:26:54 +01:00
LG-ThinQ-Integration
23c167da1b Bump thinqconnect to 1.0.9 (#157607) 2025-12-01 09:26:01 +01:00
Jan Bouwhuis
34d6938171 Fix subentry ID is not updated when renaming the entity ID (#157498) 2025-12-01 07:55:13 +01:00
Kevin Stillhammer
4bb8590076 Revert "Force httpx client to use IPv4 for waze_travel_time" (#157596) 2025-12-01 06:35:26 +01:00
Norbert Rittel
5e0923b60d Fix spelling of "to set up" in hue_ble (#157593) 2025-12-01 06:33:18 +01:00
Norbert Rittel
ad48f3c634 Fix spelling of "to log in" in anglian_water (#157594) 2025-12-01 06:32:54 +01:00
cdnninja
2bdd6854eb Bump pyvesync to 3.3.2 (#157605) 2025-11-30 23:41:46 -05:00
Lukas
0bf906911c pooldose bump to api 0.8.1 (#157591) 2025-11-30 23:49:40 +01:00
Ludovic BOUÉ
874d6f5613 Add Matter fixture for Eufy vacuum Omni E28 (#157590) 2025-11-30 21:47:31 +01:00
Raphael Hehl
43ba10eebd Add missing translations for UniFi Protect integration (#157570) 2025-11-30 17:05:05 +01:00
Sanjay Govind
64bed19805 Bump bosch-alarm-mode2 to v0.4.10 (#157564) 2025-11-30 16:02:43 +01:00
Shay Levy
6357067f0f Rename Shelly SENSORS to BLOCK_SENSORS to match naming in other platforms (#157553) 2025-11-30 12:48:35 +02:00
Thomas55555
e328ba4045 Bump google air quality api to 1.1.3 (#157555) 2025-11-30 07:17:36 +01:00
Allen Porter
332dbddce6 Bump google-nest-sdm to 9.1.1 (#157562) 2025-11-29 23:19:44 -05:00
J. Nick Koston
82d935a819 Bump aioesphomeapi to 42.9.0 (#157558) 2025-11-29 18:04:55 -06:00
Raphael Hehl
4b84998c0c Fix UFPConfigEntry type consistency in unifiprotect (#157548) 2025-11-29 17:07:44 -06:00
Raphael Hehl
e10c1ebcf6 Fix UniFi Protect RTSP repair warnings when globally disabled (#157516) 2025-11-29 22:53:34 +02:00
Raphael Hehl
0174bad182 Add PARALLEL_UPDATES to UniFi Protect platforms (#157504) 2025-11-29 19:48:43 +01:00
Allen Porter
d5be623684 Bump python-roborock to 3.8.4 (#157538) 2025-11-29 20:34:27 +02:00
Raphael Hehl
d006b044c8 Bump uiprotect to 7.31.0 (#157543) 2025-11-29 20:33:09 +02:00
Jan Bouwhuis
fdd9571623 Fix MQTT entity cannot be renamed (#157540) 2025-11-29 19:29:54 +01:00
Shay Levy
4f4c5152b9 Refactor Shelly setup to use async_setup_entry_block for block entities (#157517) 2025-11-29 18:08:12 +02:00
Denis Shulyaka
b031a082cd Bump anthropic to 0.75.0 (#157491) 2025-11-29 14:35:30 +01:00
Shay Levy
a1132195fd Refactor Shelly RPC event platform to use base class (#157499) 2025-11-29 13:09:32 +02:00
Jordan Harvey
708b3dc8b2 Disable cookie quotes for Anglian Water (#157518) 2025-11-29 11:52:55 +01:00
J. Nick Koston
8ae0216135 Bump ESPHome stable BLE version to 2025.11.0 (#157511) 2025-11-29 03:40:22 -06:00
113 changed files with 3329 additions and 1041 deletions

View File

@@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@02c6cc30ae592ce65ee356387748dfc2fd5f7993 # v2.0.3
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o
system-prompt: |

View File

@@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@02c6cc30ae592ce65ee356387748dfc2fd5f7993 # v2.0.3
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o-mini
system-prompt: |

View File

@@ -160,7 +160,6 @@
"triggers": {
"armed": {
"description": "Triggers when an alarm is armed.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
@@ -171,7 +170,6 @@
},
"armed_away": {
"description": "Triggers when an alarm is armed away.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_away::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
@@ -182,7 +180,6 @@
},
"armed_home": {
"description": "Triggers when an alarm is armed home.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_home::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
@@ -193,7 +190,6 @@
},
"armed_night": {
"description": "Triggers when an alarm is armed night.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_night::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
@@ -204,7 +200,6 @@
},
"armed_vacation": {
"description": "Triggers when an alarm is armed vacation.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_vacation::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
@@ -215,7 +210,6 @@
},
"disarmed": {
"description": "Triggers when an alarm is disarmed.",
"description_configured": "[%key:component::alarm_control_panel::triggers::disarmed::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
@@ -226,7 +220,6 @@
},
"triggered": {
"description": "Triggers when an alarm is triggered.",
"description_configured": "[%key:component::alarm_control_panel::triggers::triggered::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from aiohttp import CookieJar
from pyanglianwater import AnglianWater
from pyanglianwater.auth import MSOB2CAuth
from pyanglianwater.exceptions import (
@@ -18,7 +19,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
from .coordinator import AnglianWaterConfigEntry, AnglianWaterUpdateCoordinator
@@ -33,7 +34,10 @@ async def async_setup_entry(
auth = MSOB2CAuth(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=async_get_clientsession(hass),
session=async_create_clientsession(
hass,
cookie_jar=CookieJar(quote_cookie=False),
),
refresh_token=entry.data[CONF_ACCESS_TOKEN],
account_number=entry.data[CONF_ACCOUNT_NUMBER],
)

View File

@@ -19,7 +19,7 @@
"data_description": {
"account_number": "Your account number found on your latest bill.",
"password": "Your password",
"username": "Username or email used to login to the Anglian Water website."
"username": "Username or email used to log in to the Anglian Water website."
},
"description": "Enter your Anglian Water account credentials to connect to Home Assistant."
}

View File

@@ -421,6 +421,8 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
)
if short_form.search(model_alias):
model_alias += "-0"
if model_alias.endswith(("haiku", "opus", "sonnet")):
model_alias += "-latest"
model_options.append(
SelectOptionDict(
label=model_info.display_name,

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["anthropic==0.73.0"]
"requirements": ["anthropic==0.75.0"]
}

View File

@@ -113,7 +113,6 @@
"triggers": {
"idle": {
"description": "Triggers when an Assist satellite becomes idle.",
"description_configured": "[%key:component::assist_satellite::triggers::idle::description%]",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
@@ -124,7 +123,6 @@
},
"listening": {
"description": "Triggers when an Assist satellite starts listening.",
"description_configured": "[%key:component::assist_satellite::triggers::listening::description%]",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
@@ -135,7 +133,6 @@
},
"processing": {
"description": "Triggers when an Assist satellite is processing.",
"description_configured": "[%key:component::assist_satellite::triggers::processing::description%]",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
@@ -146,7 +143,6 @@
},
"responding": {
"description": "Triggers when an Assist satellite is responding.",
"description_configured": "[%key:component::assist_satellite::triggers::responding::description%]",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",

View File

@@ -68,9 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
name=f"Bosch {panel.model}",
name=f"Bosch {panel.model.name}",
manufacturer="Bosch Security Systems",
model=panel.model,
model=panel.model.name,
sw_version=panel.firmware_version,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -83,7 +83,7 @@ async def try_connect(
finally:
await panel.disconnect()
return (panel.model, panel.serial_number)
return (panel.model.name, panel.serial_number)
class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):

View File

@@ -20,7 +20,8 @@ async def async_get_config_entry_diagnostics(
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"data": {
"model": entry.runtime_data.model,
"model": entry.runtime_data.model.name,
"family": entry.runtime_data.model.family.name,
"serial_number": entry.runtime_data.serial_number,
"protocol_version": entry.runtime_data.protocol_version,
"firmware_version": entry.runtime_data.firmware_version,

View File

@@ -26,7 +26,7 @@ class BoschAlarmEntity(Entity):
self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=f"Bosch {panel.model}",
name=f"Bosch {panel.model.name}",
manufacturer="Bosch Security Systems",
)

View File

@@ -12,5 +12,5 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["bosch-alarm-mode2==0.4.6"]
"requirements": ["bosch-alarm-mode2==0.4.10"]
}

View File

@@ -300,7 +300,6 @@
"triggers": {
"started_cooling": {
"description": "Triggers when a climate started cooling.",
"description_configured": "[%key:component::climate::triggers::started_cooling::description%]",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
@@ -311,7 +310,6 @@
},
"started_drying": {
"description": "Triggers when a climate started drying.",
"description_configured": "[%key:component::climate::triggers::started_drying::description%]",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
@@ -322,7 +320,6 @@
},
"started_heating": {
"description": "Triggers when a climate starts to heat.",
"description_configured": "[%key:component::climate::triggers::started_heating::description%]",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
@@ -333,7 +330,6 @@
},
"turned_off": {
"description": "Triggers when a climate is turned off.",
"description_configured": "[%key:component::climate::triggers::turned_off::description%]",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
@@ -344,7 +340,6 @@
},
"turned_on": {
"description": "Triggers when a climate is turned on.",
"description_configured": "[%key:component::climate::triggers::turned_on::description%]",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",

View File

@@ -108,34 +108,5 @@
"toggle_cover_tilt": {
"service": "mdi:arrow-top-right-bottom-left"
}
},
"triggers": {
"awning_opened": {
"trigger": "mdi:awning-outline"
},
"blind_opened": {
"trigger": "mdi:blinds-horizontal"
},
"curtain_opened": {
"trigger": "mdi:curtains"
},
"door_opened": {
"trigger": "mdi:door-open"
},
"garage_opened": {
"trigger": "mdi:garage-open"
},
"gate_opened": {
"trigger": "mdi:gate-open"
},
"shade_opened": {
"trigger": "mdi:roller-shade"
},
"shutter_opened": {
"trigger": "mdi:window-shutter-open"
},
"window_opened": {
"trigger": "mdi:window-open"
}
}
}

View File

@@ -1,16 +1,4 @@
{
"common": {
"trigger_behavior_description_awning": "The behavior of the targeted awnings to trigger on.",
"trigger_behavior_description_blind": "The behavior of the targeted blinds to trigger on.",
"trigger_behavior_description_curtain": "The behavior of the targeted curtains to trigger on.",
"trigger_behavior_description_door": "The behavior of the targeted doors to trigger on.",
"trigger_behavior_description_garage": "The behavior of the targeted garage doors to trigger on.",
"trigger_behavior_description_gate": "The behavior of the targeted gates to trigger on.",
"trigger_behavior_description_shade": "The behavior of the targeted shades to trigger on.",
"trigger_behavior_description_shutter": "The behavior of the targeted shutters to trigger on.",
"trigger_behavior_description_window": "The behavior of the targeted windows to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"action_type": {
"close": "Close {entity_name}",
@@ -94,15 +82,6 @@
"name": "Window"
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"close_cover": {
"description": "Closes a cover.",
@@ -157,142 +136,5 @@
"name": "Toggle tilt"
}
},
"title": "Cover",
"triggers": {
"awning_opened": {
"description": "Triggers when an awning opens.",
"description_configured": "[%key:component::cover::triggers::awning_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_awning%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the awnings to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When an awning opens"
},
"blind_opened": {
"description": "Triggers when a blind opens.",
"description_configured": "[%key:component::cover::triggers::blind_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_blind%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the blinds to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a blind opens"
},
"curtain_opened": {
"description": "Triggers when a curtain opens.",
"description_configured": "[%key:component::cover::triggers::curtain_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_curtain%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the curtains to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a curtain opens"
},
"door_opened": {
"description": "Triggers when a door opens.",
"description_configured": "[%key:component::cover::triggers::door_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_door%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the doors to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a door opens"
},
"garage_opened": {
"description": "Triggers when a garage door opens.",
"description_configured": "[%key:component::cover::triggers::garage_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_garage%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the garage doors to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a garage door opens"
},
"gate_opened": {
"description": "Triggers when a gate opens.",
"description_configured": "[%key:component::cover::triggers::gate_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_gate%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the gates to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a gate opens"
},
"shade_opened": {
"description": "Triggers when a shade opens.",
"description_configured": "[%key:component::cover::triggers::shade_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_shade%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the shades to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a shade opens"
},
"shutter_opened": {
"description": "Triggers when a shutter opens.",
"description_configured": "[%key:component::cover::triggers::shutter_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_shutter%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the shutters to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a shutter opens"
},
"window_opened": {
"description": "Triggers when a window opens.",
"description_configured": "[%key:component::cover::triggers::window_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_window%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the windows to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a window opens"
}
}
"title": "Cover"
}

View File

@@ -1,116 +0,0 @@
"""Provides triggers for covers."""
from typing import Final
import voluptuous as vol
from homeassistant.const import CONF_OPTIONS
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
EntityTriggerBase,
Trigger,
TriggerConfig,
)
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from . import ATTR_CURRENT_POSITION, CoverDeviceClass, CoverState
from .const import DOMAIN
ATTR_FULLY_OPENED: Final = "fully_opened"
COVER_OPENED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
{
vol.Required(CONF_OPTIONS): {
vol.Required(ATTR_FULLY_OPENED, default=False): bool,
},
}
)
def get_device_class_or_undefined(
hass: HomeAssistant, entity_id: str
) -> str | None | UndefinedType:
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return get_device_class(hass, entity_id)
except HomeAssistantError:
return UNDEFINED
class CoverOpenedClosedTrigger(EntityTriggerBase):
"""Class for cover opened and closed triggers."""
_attribute: str = ATTR_CURRENT_POSITION
_attribute_value: int | None = None
_device_class: CoverDeviceClass | None
_domain: str = DOMAIN
_to_states: set[str]
def is_to_state(self, state: State) -> bool:
"""Check if the state matches the target state."""
if state.state not in self._to_states:
return False
if (
self._attribute_value is not None
and (value := state.attributes.get(self._attribute)) is not None
and value != self._attribute_value
):
return False
return True
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if get_device_class_or_undefined(self._hass, entity_id)
== self._device_class
}
class CoverOpenedTrigger(CoverOpenedClosedTrigger):
"""Class for cover opened triggers."""
_schema = COVER_OPENED_TRIGGER_SCHEMA
_to_states = {CoverState.OPEN, CoverState.OPENING}
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize the state trigger."""
super().__init__(hass, config)
if self._options.get(ATTR_FULLY_OPENED):
self._attribute_value = 100
def make_cover_opened_trigger(
device_class: CoverDeviceClass | None,
) -> type[CoverOpenedTrigger]:
"""Create an entity state attribute trigger class."""
class CustomTrigger(CoverOpenedTrigger):
"""Trigger for entity state changes."""
_device_class = device_class
return CustomTrigger
TRIGGERS: dict[str, type[Trigger]] = {
"awning_opened": make_cover_opened_trigger(CoverDeviceClass.AWNING),
"blind_opened": make_cover_opened_trigger(CoverDeviceClass.BLIND),
"curtain_opened": make_cover_opened_trigger(CoverDeviceClass.CURTAIN),
"door_opened": make_cover_opened_trigger(CoverDeviceClass.DOOR),
"garage_opened": make_cover_opened_trigger(CoverDeviceClass.GARAGE),
"gate_opened": make_cover_opened_trigger(CoverDeviceClass.GATE),
"shade_opened": make_cover_opened_trigger(CoverDeviceClass.SHADE),
"shutter_opened": make_cover_opened_trigger(CoverDeviceClass.SHUTTER),
"window_opened": make_cover_opened_trigger(CoverDeviceClass.WINDOW),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for covers."""
return TRIGGERS

View File

@@ -1,79 +0,0 @@
.trigger_common_fields: &trigger_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
fully_opened:
required: true
default: false
selector:
boolean:
awning_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: awning
blind_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: blind
curtain_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: curtain
door_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: door
garage_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: garage
gate_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: gate
shade_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: shade
shutter_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: shutter
window_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: window

View File

@@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
DEFAULT_PORT: Final = 6053
STABLE_BLE_VERSION_STR = "2025.8.0"
STABLE_BLE_VERSION_STR = "2025.11.0"
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
PROJECT_URLS = {
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==42.8.0",
"aioesphomeapi==42.9.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.4.0"
],

View File

@@ -157,7 +157,7 @@
"title": "[%key:component::assist_pipeline::issues::assist_in_progress_deprecated::title%]"
},
"ble_firmware_outdated": {
"description": "To improve Bluetooth reliability and performance, we highly recommend updating {name} with ESPHome {version} or later. When updating the device from ESPHome earlier than 2022.12.0, it is recommended to use a serial cable instead of an over-the-air update to take advantage of the new partition scheme.",
"description": "ESPHome {version} introduces ultra-low latency event processing, reducing BLE event delays from 0-16 milliseconds to approximately 12 microseconds. This resolves stability issues when pairing, connecting, or handshaking with devices that require low latency, and makes Bluetooth proxy operations rival or exceed local adapters. We highly recommend updating {name} to take advantage of these improvements.",
"title": "Update {name} with ESPHome {version} or later"
},
"device_conflict": {

View File

@@ -167,7 +167,6 @@
"triggers": {
"turned_off": {
"description": "Triggers when a fan is turned off.",
"description_configured": "[%key:component::fan::triggers::turned_off::description%]",
"fields": {
"behavior": {
"description": "[%key:component::fan::common::trigger_behavior_description%]",
@@ -178,7 +177,6 @@
},
"turned_on": {
"description": "Triggers when a fan is turned on.",
"description_configured": "[%key:component::fan::triggers::turned_on::description%]",
"fields": {
"behavior": {
"description": "[%key:component::fan::common::trigger_behavior_description%]",

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_air_quality_api"],
"quality_scale": "bronze",
"requirements": ["google_air_quality_api==1.1.2"]
"requirements": ["google_air_quality_api==1.1.3"]
}

View File

@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"not_implemented": "This integration can only be setup via discovery."
"not_implemented": "This integration can only be set up via discovery."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",

View File

@@ -42,7 +42,6 @@
"triggers": {
"docked": {
"description": "Triggers when a lawn mower has docked.",
"description_configured": "[%key:component::lawn_mower::triggers::docked::description%]",
"fields": {
"behavior": {
"description": "[%key:component::lawn_mower::common::trigger_behavior_description%]",
@@ -53,7 +52,6 @@
},
"errored": {
"description": "Triggers when a lawn mower has errored.",
"description_configured": "[%key:component::lawn_mower::triggers::errored::description%]",
"fields": {
"behavior": {
"description": "[%key:component::lawn_mower::common::trigger_behavior_description%]",
@@ -64,7 +62,6 @@
},
"paused_mowing": {
"description": "Triggers when a lawn mower has paused mowing.",
"description_configured": "[%key:component::lawn_mower::triggers::paused_mowing::description%]",
"fields": {
"behavior": {
"description": "[%key:component::lawn_mower::common::trigger_behavior_description%]",
@@ -75,7 +72,6 @@
},
"started_mowing": {
"description": "Triggers when a lawn mower has started mowing.",
"description_configured": "[%key:component::lawn_mower::triggers::started_mowing::description%]",
"fields": {
"behavior": {
"description": "[%key:component::lawn_mower::common::trigger_behavior_description%]",

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
"iot_class": "cloud_push",
"loggers": ["thinqconnect"],
"requirements": ["thinqconnect==1.0.8"]
"requirements": ["thinqconnect==1.0.9"]
}

View File

@@ -43,7 +43,6 @@
"conditions": {
"is_off": {
"description": "Test if a light is off.",
"description_configured": "[%key:component::light::conditions::is_off::description%]",
"fields": {
"behavior": {
"description": "[%key:component::light::common::condition_behavior_description%]",
@@ -54,7 +53,6 @@
},
"is_on": {
"description": "Test if a light is on.",
"description_configured": "[%key:component::light::conditions::is_on::description%]",
"fields": {
"behavior": {
"description": "[%key:component::light::common::condition_behavior_description%]",
@@ -513,7 +511,6 @@
"triggers": {
"turned_off": {
"description": "Triggers when a light is turned off.",
"description_configured": "[%key:component::light::triggers::turned_off::description%]",
"fields": {
"behavior": {
"description": "[%key:component::light::common::trigger_behavior_description%]",
@@ -524,7 +521,6 @@
},
"turned_on": {
"description": "Triggers when a light is turned on.",
"description_configured": "[%key:component::light::triggers::turned_on::description%]",
"fields": {
"behavior": {
"description": "[%key:component::light::common::trigger_behavior_description%]",

View File

@@ -499,4 +499,53 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterBinarySensor,
required_attributes=(clusters.WindowCovering.Attributes.ConfigStatus,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="ThermostatRemoteSensing_LocalTemperature",
translation_key="thermostat_remote_sensing_local_temperature",
entity_category=EntityCategory.DIAGNOSTIC,
# LocalTemperature bit from RemoteSensing attribute
device_to_ha=lambda x: bool(
x
& clusters.Thermostat.Bitmaps.RemoteSensingBitmap.kLocalTemperature # Calculated Local Temperature is derived from a remote node
),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.Thermostat.Attributes.RemoteSensing,),
allow_multi=True,
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="ThermostatRemoteSensing_OutdoorTemperature",
translation_key="thermostat_remote_sensing_outdoor_temperature",
entity_category=EntityCategory.DIAGNOSTIC,
# OutdoorTemperature bit from RemoteSensing attribute
device_to_ha=lambda x: bool(
x
& clusters.Thermostat.Bitmaps.RemoteSensingBitmap.kOutdoorTemperature # OutdoorTemperature is derived from a remote node
),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.Thermostat.Attributes.RemoteSensing,),
allow_multi=True,
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="ThermostatRemoteSensing_Occupancy",
translation_key="thermostat_remote_sensing_occupancy",
entity_category=EntityCategory.DIAGNOSTIC,
# Occupancy bit from RemoteSensing attribute
device_to_ha=lambda x: bool(
x
& clusters.Thermostat.Bitmaps.RemoteSensingBitmap.kOccupancy # Occupancy is derived from a remote node
),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.Thermostat.Attributes.RemoteSensing,),
featuremap_contains=clusters.Thermostat.Bitmaps.Feature.kOccupancy,
allow_multi=True,
),
]

View File

@@ -89,6 +89,15 @@
"test_in_progress": {
"name": "Test in progress"
},
"thermostat_remote_sensing_local_temperature": {
"name": "Local temperature remote sensing"
},
"thermostat_remote_sensing_occupancy": {
"name": "Occupancy remote sensing"
},
"thermostat_remote_sensing_outdoor_temperature": {
"name": "Outdoor temperature remote sensing"
},
"valve_fault_blocked": {
"name": "Valve blocked"
},

View File

@@ -382,7 +382,6 @@
"triggers": {
"stopped_playing": {
"description": "Triggers when a media player stops playing.",
"description_configured": "[%key:component::media_player::triggers::stopped_playing::description%]",
"fields": {
"behavior": {
"description": "[%key:component::media_player::common::trigger_behavior_description%]",

View File

@@ -1486,6 +1486,7 @@ class MqttEntity(
entity_registry.async_update_entity(
self.entity_id, new_entity_id=self._update_registry_entity_id
)
self._update_registry_entity_id = None
await super().async_added_to_hass()
self._subscriptions = {}

View File

@@ -1563,7 +1563,6 @@
"triggers": {
"_": {
"description": "When a specific message is received on a given MQTT topic.",
"description_configured": "When an MQTT message has been received",
"fields": {
"payload": {
"description": "The payload to trigger on.",

View File

@@ -19,5 +19,5 @@
"documentation": "https://www.home-assistant.io/integrations/nest",
"iot_class": "cloud_push",
"loggers": ["google_nest_sdm"],
"requirements": ["google-nest-sdm==9.1.0"]
"requirements": ["google-nest-sdm==9.1.1"]
}

View File

@@ -2,17 +2,20 @@
from __future__ import annotations
from homeassistant.const import UnitOfTemperature, UnitOfVolumeFlowRate
from homeassistant.const import UnitOfTemperature, UnitOfVolume, UnitOfVolumeFlowRate
DOMAIN = "pooldose"
MANUFACTURER = "SEKO"
# Mapping of device units to Home Assistant units
# Mapping of device units (upper case) to Home Assistant units
UNIT_MAPPING: dict[str, str] = {
# Temperature units
"°C": UnitOfTemperature.CELSIUS,
"°F": UnitOfTemperature.FAHRENHEIT,
# Volume flow rate units
"m3/h": UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
"L/s": UnitOfVolumeFlowRate.LITERS_PER_SECOND,
"M3/H": UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
"L/S": UnitOfVolumeFlowRate.LITERS_PER_SECOND,
# Volume units
"L": UnitOfVolume.LITERS,
"M3": UnitOfVolume.CUBIC_METERS,
}

View File

@@ -119,6 +119,9 @@
},
"ph_type_dosing": {
"default": "mdi:beaker"
},
"water_meter_total_permanent": {
"default": "mdi:counter"
}
},
"switch": {

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/pooldose",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["python-pooldose==0.8.0"]
"requirements": ["python-pooldose==0.8.1"]
}

View File

@@ -10,6 +10,7 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
@@ -58,6 +59,13 @@ SENSOR_DESCRIPTIONS: tuple[PooldoseSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
use_dynamic_unit=True,
),
PooldoseSensorEntityDescription(
key="water_meter_total_permanent",
translation_key="water_meter_total_permanent",
device_class=SensorDeviceClass.VOLUME,
state_class=SensorStateClass.TOTAL_INCREASING,
use_dynamic_unit=True,
),
PooldoseSensorEntityDescription(
key="ph_type_dosing",
translation_key="ph_type_dosing",
@@ -223,8 +231,8 @@ class PooldoseSensor(PooldoseEntity, SensorEntity):
and (data := self.get_data()) is not None
and (device_unit := data.get("unit"))
):
# Map device unit to Home Assistant unit, return None if unknown
return UNIT_MAPPING.get(device_unit)
# Map device unit (upper case) to Home Assistant unit, return None if unknown
return UNIT_MAPPING.get(device_unit.upper())
# Fall back to static unit from entity description
return super().native_unit_of_measurement

View File

@@ -160,6 +160,9 @@
"acid": "pH-",
"alcalyne": "pH+"
}
},
"water_meter_total_permanent": {
"name": "Totalizer"
}
},
"switch": {

View File

@@ -19,7 +19,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==3.8.3",
"python-roborock==3.8.4",
"vacuum-map-parser-roborock==0.1.4"
]
}

View File

@@ -30,7 +30,7 @@ from .entity import (
ShellyRpcAttributeEntity,
ShellySleepingBlockAttributeEntity,
ShellySleepingRpcAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rest,
async_setup_entry_rpc,
)
@@ -127,7 +127,7 @@ class RpcBluTrvBinarySensor(RpcBinarySensor):
)
SENSORS: dict[tuple[str, str], BlockBinarySensorDescription] = {
BLOCK_SENSORS: dict[tuple[str, str], BlockBinarySensorDescription] = {
("device", "overtemp"): BlockBinarySensorDescription(
key="device|overtemp",
translation_key="overheating",
@@ -372,19 +372,19 @@ def _async_setup_block_entry(
) -> None:
"""Set up entities for BLOCK device."""
if config_entry.data[CONF_SLEEP_PERIOD]:
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockSleepingBinarySensor,
)
else:
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockBinarySensor,
)
async_setup_entry_rest(

View File

@@ -341,3 +341,5 @@ MODEL_TOP_EV_CHARGER_EVE01 = "EVE01"
MODEL_FRANKEVER_IRRIGATION_CONTROLLER = "Irrigation"
ROLE_GENERIC = "generic"
TRV_CHANNEL = 0

View File

@@ -27,7 +27,7 @@ from .entity import (
RpcEntityDescription,
ShellyBlockAttributeEntity,
ShellyRpcAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rpc,
rpc_call,
)
@@ -81,7 +81,7 @@ def _async_setup_block_entry(
coordinator = config_entry.runtime_data.block
assert coordinator
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass, config_entry, async_add_entities, BLOCK_COVERS, BlockShellyCover
)

View File

@@ -34,14 +34,14 @@ from .utils import (
@callback
def async_setup_entry_attribute_entities(
def async_setup_entry_block(
hass: HomeAssistant,
config_entry: ShellyConfigEntry,
async_add_entities: AddEntitiesCallback,
sensors: Mapping[tuple[str, str], BlockEntityDescription],
sensor_class: Callable,
) -> None:
"""Set up entities for attributes."""
"""Set up block entities."""
coordinator = config_entry.runtime_data.block
assert coordinator
if coordinator.device.initialized:
@@ -150,7 +150,7 @@ def async_setup_entry_rpc(
sensors: Mapping[str, RpcEntityDescription],
sensor_class: Callable,
) -> None:
"""Set up entities for RPC sensors."""
"""Set up RPC entities."""
coordinator = config_entry.runtime_data.rpc
assert coordinator

View File

@@ -18,7 +18,6 @@ from homeassistant.components.event import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
BASIC_INPUTS_EVENTS_TYPES,
@@ -26,7 +25,7 @@ from .const import (
SHIX3_1_INPUTS_EVENTS_TYPES,
)
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
from .entity import ShellyBlockEntity, get_entity_rpc_device_info
from .entity import ShellyBlockEntity, ShellyRpcEntity
from .utils import (
async_remove_orphaned_entities,
async_remove_shelly_entity,
@@ -136,7 +135,7 @@ def _async_setup_rpc_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up entities for RPC device."""
entities: list[ShellyRpcEvent] = []
entities: list[ShellyRpcEvent | ShellyRpcScriptEvent] = []
coordinator = config_entry.runtime_data.rpc
if TYPE_CHECKING:
@@ -162,7 +161,9 @@ def _async_setup_rpc_entry(
continue
if script_events and (event_types := script_events[get_rpc_key_id(script)]):
entities.append(ShellyRpcScriptEvent(coordinator, script, event_types))
entities.append(
ShellyRpcScriptEvent(coordinator, script, SCRIPT_EVENT, event_types)
)
# If a script is removed, from the device configuration, we need to remove orphaned entities
async_remove_orphaned_entities(
@@ -227,7 +228,7 @@ class ShellyBlockEvent(ShellyBlockEntity, EventEntity):
self.async_write_ha_state()
class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
class ShellyRpcEvent(ShellyRpcEntity, EventEntity):
"""Represent RPC event entity."""
_attr_has_entity_name = True
@@ -240,25 +241,19 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
description: ShellyRpcEventDescription,
) -> None:
"""Initialize Shelly entity."""
super().__init__(coordinator)
self._attr_device_info = get_entity_rpc_device_info(coordinator, key)
self._attr_unique_id = f"{coordinator.mac}-{key}"
super().__init__(coordinator, key)
self.entity_description = description
if description.key == "input":
_, component, component_id = get_rpc_key(key)
if custom_name := get_rpc_custom_name(coordinator.device, key):
self._attr_name = custom_name
else:
self._attr_translation_placeholders = {
"input_number": component_id
if get_rpc_number_of_channels(coordinator.device, component) > 1
else ""
}
self.event_id = int(component_id)
elif description.key == "script":
self._attr_name = get_rpc_custom_name(coordinator.device, key)
self.event_id = get_rpc_key_id(key)
_, component, component_id = get_rpc_key(key)
if custom_name := get_rpc_custom_name(coordinator.device, key):
self._attr_name = custom_name
else:
self._attr_translation_placeholders = {
"input_number": component_id
if get_rpc_number_of_channels(coordinator.device, component) > 1
else ""
}
self.event_id = int(component_id)
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
@@ -270,30 +265,36 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
@callback
def _async_handle_event(self, event: dict[str, Any]) -> None:
"""Handle the demo button event."""
"""Handle the event."""
if event["id"] == self.event_id:
self._trigger_event(event["event"])
self.async_write_ha_state()
class ShellyRpcScriptEvent(ShellyRpcEvent):
class ShellyRpcScriptEvent(ShellyRpcEntity, EventEntity):
"""Represent RPC script event entity."""
_attr_has_entity_name = True
entity_description: ShellyRpcEventDescription
def __init__(
self,
coordinator: ShellyRpcCoordinator,
key: str,
description: ShellyRpcEventDescription,
event_types: list[str],
) -> None:
"""Initialize Shelly script event entity."""
super().__init__(coordinator, key, SCRIPT_EVENT)
self.component = key
super().__init__(coordinator, key)
self.entity_description = description
self._attr_event_types = event_types
self._attr_name = get_rpc_custom_name(coordinator.device, key)
self.event_id = get_rpc_key_id(key)
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super(CoordinatorEntity, self).async_added_to_hass()
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.async_subscribe_events(self._async_handle_event)
@@ -302,7 +303,7 @@ class ShellyRpcScriptEvent(ShellyRpcEvent):
@callback
def _async_handle_event(self, event: dict[str, Any]) -> None:
"""Handle script event."""
if event.get("component") == self.component:
if event.get("component") == self.key:
event_type = event.get("event")
if event_type not in self.event_types:
# This can happen if we didn't find this event type in the script

View File

@@ -44,7 +44,7 @@ from .entity import (
RpcEntityDescription,
ShellyBlockAttributeEntity,
ShellyRpcAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rpc,
)
from .utils import (
@@ -101,7 +101,7 @@ def _async_setup_block_entry(
coordinator = config_entry.runtime_data.block
assert coordinator
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass, config_entry, async_add_entities, BLOCK_LIGHTS, BlockShellyLight
)

View File

@@ -17,7 +17,7 @@
"iot_class": "local_push",
"loggers": ["aioshelly"],
"quality_scale": "platinum",
"requirements": ["aioshelly==13.21.0"],
"requirements": ["aioshelly==13.22.0"],
"zeroconf": [
{
"name": "shelly*",

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Final, cast
from typing import TYPE_CHECKING, Final, cast
from aioshelly.block_device import Block
from aioshelly.const import RPC_GENERATIONS
@@ -34,6 +34,7 @@ from .const import (
MODEL_LINKEDGO_ST1820_THERMOSTAT,
MODEL_TOP_EV_CHARGER_EVE01,
ROLE_GENERIC,
TRV_CHANNEL,
VIRTUAL_NUMBER_MODE_MAP,
)
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
@@ -42,7 +43,7 @@ from .entity import (
RpcEntityDescription,
ShellyRpcAttributeEntity,
ShellySleepingBlockAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rpc,
rpc_call,
)
@@ -62,9 +63,6 @@ PARALLEL_UPDATES = 0
class BlockNumberDescription(BlockEntityDescription, NumberEntityDescription):
"""Class to describe a BLOCK sensor."""
rest_path: str = ""
rest_arg: str = ""
@dataclass(frozen=True, kw_only=True)
class RpcNumberDescription(RpcEntityDescription, NumberEntityDescription):
@@ -178,7 +176,7 @@ class RpcBluTrvExtTempNumber(RpcBluTrvNumber):
self.async_write_ha_state()
NUMBERS: dict[tuple[str, str], BlockNumberDescription] = {
BLOCK_NUMBERS: dict[tuple[str, str], BlockNumberDescription] = {
("device", "valvePos"): BlockNumberDescription(
key="device|valvepos",
translation_key="valve_position",
@@ -189,8 +187,6 @@ NUMBERS: dict[tuple[str, str], BlockNumberDescription] = {
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
rest_path="thermostat/0",
rest_arg="pos",
),
}
@@ -353,11 +349,11 @@ def _async_setup_block_entry(
) -> None:
"""Set up entities for BLOCK device."""
if config_entry.data[CONF_SLEEP_PERIOD]:
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,
NUMBERS,
BLOCK_NUMBERS,
BlockSleepingNumber,
)
@@ -426,18 +422,11 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, RestoreNumber):
async def async_set_native_value(self, value: float) -> None:
"""Set value."""
# Example for Shelly Valve: http://192.168.188.187/thermostat/0?pos=13.0
await self._set_state_full_path(
self.entity_description.rest_path,
{self.entity_description.rest_arg: value},
LOGGER.debug(
"Setting thermostat position for entity %s to %s", self.name, value
)
self.async_write_ha_state()
async def _set_state_full_path(self, path: str, params: Any) -> Any:
"""Set block state (HTTP request)."""
LOGGER.debug("Setting state for entity %s, state: %s", self.name, params)
try:
return await self.coordinator.device.http_request("get", path, params)
await self.coordinator.device.set_thermostat_state(TRV_CHANNEL, pos=value)
except DeviceConnectionError as err:
self.coordinator.last_update_success = False
raise HomeAssistantError(
@@ -450,3 +439,4 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, RestoreNumber):
) from err
except InvalidAuthError:
await self.coordinator.async_shutdown_device_and_start_reauth()
self.async_write_ha_state()

View File

@@ -53,7 +53,7 @@ from .entity import (
ShellyRpcAttributeEntity,
ShellySleepingBlockAttributeEntity,
ShellySleepingRpcAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rest,
async_setup_entry_rpc,
get_entity_rpc_device_info,
@@ -198,7 +198,7 @@ class RpcBluTrvSensor(RpcSensor):
)
SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
BLOCK_SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
("device", "battery"): BlockSensorDescription(
key="device|battery",
native_unit_of_measurement=PERCENTAGE,
@@ -525,7 +525,6 @@ RPC_SENSORS: Final = {
"power_rgbcct": RpcSensorDescription(
key="rgbcct",
sub_key="apower",
name="Power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
@@ -963,7 +962,6 @@ RPC_SENSORS: Final = {
"energy_rgbcct": RpcSensorDescription(
key="rgbcct",
sub_key="aenergy",
name="Energy",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value=lambda status, _: status["total"],
@@ -1736,19 +1734,19 @@ def _async_setup_block_entry(
) -> None:
"""Set up entities for BLOCK device."""
if config_entry.data[CONF_SLEEP_PERIOD]:
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockSleepingSensor,
)
else:
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,
SENSORS,
BLOCK_SENSORS,
BlockSensor,
)
async_setup_entry_rest(

View File

@@ -36,7 +36,7 @@ from .entity import (
ShellyBlockAttributeEntity,
ShellyRpcAttributeEntity,
ShellySleepingBlockAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_block,
async_setup_entry_rpc,
rpc_call,
)
@@ -337,11 +337,11 @@ def _async_setup_block_entry(
coordinator = config_entry.runtime_data.block
assert coordinator
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass, config_entry, async_add_entities, BLOCK_RELAY_SWITCHES, BlockRelaySwitch
)
async_setup_entry_attribute_entities(
async_setup_entry_block(
hass,
config_entry,
async_add_entities,

View File

@@ -33,9 +33,6 @@
},
"solar_elevation": {
"default": "mdi:theme-light-dark"
},
"solar_rising": {
"default": "mdi:sun-clock"
}
}
}

View File

@@ -18,11 +18,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.typing import StateType
from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED
@@ -100,13 +95,6 @@ SENSOR_TYPES: tuple[SunSensorEntityDescription, ...] = (
native_unit_of_measurement=DEGREE,
signal=SIGNAL_POSITION_CHANGED,
),
SunSensorEntityDescription(
key="solar_rising",
translation_key="solar_rising",
value_fn=lambda data: data.rising,
entity_registry_enabled_default=False,
signal=SIGNAL_EVENTS_CHANGED,
),
)
@@ -155,20 +143,6 @@ class SunSensor(SensorEntity):
"""Register signal listener when added to hass."""
await super().async_added_to_hass()
if self.entity_description.key == "solar_rising":
async_create_issue(
self.hass,
DOMAIN,
"deprecated_sun_solar_rising",
breaks_in_ha_version="2026.1.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_sun_solar_rising",
translation_placeholders={
"entity": self.entity_id,
},
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
@@ -176,9 +150,3 @@ class SunSensor(SensorEntity):
self.async_write_ha_state,
)
)
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
await super().async_will_remove_from_hass()
if self.entity_description.key == "solar_rising":
async_delete_issue(self.hass, DOMAIN, "deprecated_sun_solar_rising")

View File

@@ -24,8 +24,7 @@
"next_rising": { "name": "Next rising" },
"next_setting": { "name": "Next setting" },
"solar_azimuth": { "name": "Solar azimuth" },
"solar_elevation": { "name": "Solar elevation" },
"solar_rising": { "name": "Solar rising" }
"solar_elevation": { "name": "Solar elevation" }
}
},
"entity_component": {
@@ -37,11 +36,5 @@
}
}
},
"issues": {
"deprecated_sun_solar_rising": {
"description": "The 'Solar rising' sensor of the Sun integration is being deprecated; an equivalent 'Solar rising' binary sensor has been made available as a replacement. To resolve this issue, disable {entity}.",
"title": "Deprecated 'Solar rising' sensor"
}
},
"title": "Sun"
}

View File

@@ -46,7 +46,6 @@
"triggers": {
"changed": {
"description": "Triggers when the text changes.",
"description_configured": "[%key:component::text::triggers::changed::description%]",
"name": "When the text changes"
}
}

View File

@@ -15,7 +15,7 @@ from uiprotect.exceptions import BadRequest, ClientError, NotAuthorized
# diagnostics module will not be imported in the executor.
from uiprotect.test_util.anonymize import anonymize_data # noqa: F401
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
@@ -208,7 +208,7 @@ async def async_remove_config_entry_device(
return True
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_migrate_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool:
"""Migrate entry."""
_LOGGER.debug("Migrating configuration from version %s", entry.version)

View File

@@ -39,6 +39,7 @@ from .entity import (
)
_KEY_DOOR = "door"
PARALLEL_UPDATES = 0
@dataclasses.dataclass(frozen=True, kw_only=True)

View File

@@ -33,6 +33,7 @@ from .entity import (
)
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)

View File

@@ -32,6 +32,7 @@ from .entity import ProtectDeviceEntity
from .utils import get_camera_base_name
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
@callback
@@ -91,7 +92,11 @@ def _get_camera_channels(
# no RTSP enabled use first channel with no stream
if is_default and not camera.is_third_party_camera:
_create_rtsp_repair(hass, entry, data, camera)
# Only create repair issue if RTSP is not disabled globally
if not data.disable_stream:
_create_rtsp_repair(hass, entry, data, camera)
else:
ir.async_delete_issue(hass, DOMAIN, f"rtsp_disabled_{camera.id}")
yield camera, camera.channels[0], True
else:
ir.async_delete_issue(hass, DOMAIN, f"rtsp_disabled_{camera.id}")

View File

@@ -16,7 +16,6 @@ import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_IGNORE,
ConfigEntry,
ConfigEntryState,
ConfigFlow,
ConfigFlowResult,
@@ -55,7 +54,7 @@ from .const import (
MIN_REQUIRED_PROTECT_V,
OUTDATED_LOG_MESSAGE,
)
from .data import async_last_update_was_successful
from .data import UFPConfigEntry, async_last_update_was_successful
from .discovery import async_start_discovery
from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass
@@ -80,7 +79,7 @@ def _host_is_direct_connect(host: str) -> bool:
async def _async_console_is_offline(
hass: HomeAssistant,
entry: ConfigEntry,
entry: UFPConfigEntry,
) -> bool:
"""Check if a console is offline.
@@ -224,7 +223,7 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
config_entry: UFPConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
return OptionsFlowHandler()

View File

@@ -40,6 +40,8 @@ from .data import (
)
from .entity import EventEntityMixin, ProtectDeviceEntity, ProtectEventMixin
PARALLEL_UPDATES = 0
# Select best thumbnail
# Prefer thumbnails with LPR data, sorted by confidence

View File

@@ -6,8 +6,9 @@ import logging
from typing import Any
from uiprotect.data import Light, ModelType, ProtectAdoptableDeviceModel
from uiprotect.data.devices import LightDeviceSettings
from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -15,6 +16,7 @@ from .data import ProtectDeviceType, UFPConfigEntry
from .entity import ProtectDeviceEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
async def async_setup_entry(
@@ -71,10 +73,36 @@ class ProtectLight(ProtectDeviceEntity, LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
_LOGGER.debug("Turning on light")
await self.device.api.set_light_is_led_force_on(self.device.id, True)
brightness = kwargs.get(ATTR_BRIGHTNESS)
led_level: int | None = None
if brightness is not None:
led_level = hass_to_unifi_brightness(brightness)
_LOGGER.debug(
"Turning on light with brightness %s (led_level=%s)",
brightness,
led_level,
)
else:
_LOGGER.debug("Turning on light")
await self.device.api.update_light_public(
self.device.id,
is_light_force_enabled=True,
light_device_settings=(
LightDeviceSettings(
is_indicator_enabled=self.device.light_device_settings.is_indicator_enabled,
led_level=led_level,
pir_duration=self.device.light_device_settings.pir_duration,
pir_sensitivity=self.device.light_device_settings.pir_sensitivity,
)
if led_level is not None
else None
),
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
_LOGGER.debug("Turning off light")
await self.device.api.set_light_is_led_force_on(self.device.id, False)
await self.device.api.update_light_public(
self.device.id, is_light_force_enabled=False
)

View File

@@ -20,6 +20,7 @@ from .data import ProtectDeviceType, UFPConfigEntry
from .entity import ProtectDeviceEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
async def async_setup_entry(

View File

@@ -40,7 +40,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
"requirements": ["uiprotect==7.29.0", "unifi-discovery==1.2.0"],
"requirements": ["uiprotect==7.31.0", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@@ -23,10 +23,12 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .data import ProtectDeviceType, UFPConfigEntry
from .entity import ProtectDeviceEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
_SPEAKER_DESCRIPTION = MediaPlayerEntityDescription(
key="speaker",
@@ -77,7 +79,13 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None:
super()._async_update_device_from_protect(device)
updated_device = self.device
self._attr_volume_level = float(updated_device.speaker_settings.volume / 100)
speaker_settings = updated_device.speaker_settings
volume = (
speaker_settings.speaker_volume
if speaker_settings.speaker_volume is not None
else speaker_settings.volume
)
self._attr_volume_level = float(volume / 100)
if (
updated_device.talkback_stream is not None
@@ -122,7 +130,10 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
media_id = async_process_play_media_url(self.hass, play_item.url)
if media_type != MediaType.MUSIC:
raise HomeAssistantError("Only music media type is supported")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="only_music_supported",
)
_LOGGER.debug(
"Playing Media %s for %s Speaker", media_id, self.device.display_name
@@ -131,7 +142,11 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
try:
await self.device.play_audio(media_id, blocking=False)
except StreamError as err:
raise HomeAssistantError(err) from err
_LOGGER.debug("Error playing audio: %s", err)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="stream_error",
) from err
# update state after starting player
self._async_updated_event(self.device)

View File

@@ -29,6 +29,8 @@ from .entity import (
async_all_device_entities,
)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class ProtectNumberEntityDescription(
@@ -90,6 +92,36 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ufp_set_method="set_mic_volume",
ufp_perm=PermRequired.WRITE,
),
ProtectNumberEntityDescription(
key="system_sounds_volume",
translation_key="system_sounds_volume",
icon="mdi:volume-high",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field="feature_flags.has_speaker",
ufp_value="speaker_settings.volume",
ufp_enabled="feature_flags.has_speaker",
ufp_set_method="set_volume",
ufp_perm=PermRequired.WRITE,
),
ProtectNumberEntityDescription(
key="doorbell_ring_volume",
translation_key="doorbell_ring_volume",
icon="mdi:bell-ring",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field="feature_flags.is_doorbell",
ufp_value="speaker_settings.ring_volume",
ufp_enabled="feature_flags.is_doorbell",
ufp_set_method="set_ring_volume",
ufp_perm=PermRequired.WRITE,
),
ProtectNumberEntityDescription(
key="zoom_position",
translation_key="zoom_level",

View File

@@ -10,7 +10,6 @@ import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir
@@ -165,7 +164,7 @@ class RTSPRepair(ProtectRepair):
@callback
def _async_get_or_create_api_client(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: UFPConfigEntry
) -> ProtectApiClient:
"""Get or create an API client."""
if data := async_get_data_for_entry_id(hass, entry.entry_id):

View File

@@ -45,6 +45,7 @@ from .utils import async_get_light_motion_current
_LOGGER = logging.getLogger(__name__)
_KEY_LIGHT_MOTION = "light_motion"
PARALLEL_UPDATES = 0
HDR_MODES = [
{"id": "always", "name": "Always On"},

View File

@@ -55,6 +55,7 @@ from .utils import async_get_light_motion_current
_LOGGER = logging.getLogger(__name__)
OBJECT_TYPE_NONE = "none"
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
import logging
from typing import Any, cast
from pydantic import ValidationError
@@ -45,6 +46,8 @@ from .const import (
)
from .data import async_ufp_instance_for_config_entry_ids
_LOGGER = logging.getLogger(__name__)
SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text"
SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text"
SERVICE_SET_PRIVACY_ZONE = "set_privacy_zone"
@@ -92,7 +95,11 @@ GET_USER_KEYRING_INFO_SCHEMA = vol.Schema(
def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiClient:
device_registry = dr.async_get(hass)
if not (device_entry := device_registry.async_get(device_id)):
raise HomeAssistantError(f"No device found for device id: {device_id}")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device_id": device_id},
)
if device_entry.via_device_id is not None:
return _async_get_ufp_instance(hass, device_entry.via_device_id)
@@ -101,7 +108,11 @@ def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiCl
if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids):
return ufp_instance
raise HomeAssistantError(f"No device found for device id: {device_id}")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device_id": device_id},
)
@callback
@@ -141,7 +152,11 @@ async def _async_service_call_nvr(
*(getattr(i.bootstrap.nvr, method)(*args, **kwargs) for i in instances)
)
except (ClientError, ValidationError) as err:
raise HomeAssistantError(str(err)) from err
_LOGGER.debug("Error calling UniFi Protect service: %s", err)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_error",
) from err
async def add_doorbell_text(call: ServiceCall) -> None:
@@ -170,7 +185,12 @@ async def remove_privacy_zone(call: ServiceCall) -> None:
if remove_index is None:
raise ServiceValidationError(
f"Could not find privacy zone with name {name} on camera {camera.display_name}."
translation_domain=DOMAIN,
translation_key="privacy_zone_not_found",
translation_placeholders={
"zone_name": name,
"camera_name": camera.display_name,
},
)
def remove_zone() -> None:
@@ -230,7 +250,10 @@ async def get_user_keyring_info(call: ServiceCall) -> ServiceResponse:
camera = _async_get_ufp_camera(call)
ulp_users = camera.api.bootstrap.ulp_users.as_list()
if not ulp_users:
raise HomeAssistantError("No users found, please check Protect permissions.")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="no_users_found",
)
user_keyrings: list[JsonValueType] = [
{

View File

@@ -20,7 +20,9 @@
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"api_key": "API key for your local user account."
"api_key": "[%key:component::unifiprotect::config::step::user::data_description::api_key%]",
"password": "[%key:component::unifiprotect::config::step::user::data_description::password%]",
"username": "[%key:component::unifiprotect::config::step::user::data_description::username%]"
},
"description": "Do you want to set up {name} ({ip_address})? You will need a local user created in your UniFi OS Console to log in with. Ubiquiti Cloud users will not work. For more information: {local_user_documentation_url}",
"title": "UniFi Protect discovered"
@@ -34,8 +36,11 @@
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"api_key": "API key for your local user account.",
"username": "Username for your local (not cloud) user account."
"api_key": "[%key:component::unifiprotect::config::step::user::data_description::api_key%]",
"host": "[%key:component::unifiprotect::config::step::user::data_description::host%]",
"password": "[%key:component::unifiprotect::config::step::user::data_description::password%]",
"port": "[%key:component::unifiprotect::config::step::user::data_description::port%]",
"username": "[%key:component::unifiprotect::config::step::user::data_description::username%]"
},
"description": "Your credentials or API key seem to be missing or invalid. For instructions on how to create a local user or generate a new API key, please refer to the documentation: {local_user_documentation_url}",
"title": "UniFi Protect reauth"
@@ -51,7 +56,11 @@
},
"data_description": {
"api_key": "API key for your local user account.",
"host": "Hostname or IP address of your UniFi Protect device."
"host": "Hostname or IP address of your UniFi Protect device.",
"password": "Password for your local user account.",
"port": "Port of your UniFi Protect device.",
"username": "Username for your local (not cloud) user account.",
"verify_ssl": "Verify SSL certificate of the UniFi Protect device."
},
"description": "You will need a local user created in your UniFi OS Console to log in with. Ubiquiti Cloud users will not work. For more information: {local_user_documentation_url}",
"title": "UniFi Protect setup"
@@ -280,6 +289,9 @@
"chime_duration": {
"name": "Chime duration"
},
"doorbell_ring_volume": {
"name": "Doorbell ring volume"
},
"infrared_custom_lux_trigger": {
"name": "Infrared custom lux trigger"
},
@@ -289,6 +301,9 @@
"motion_sensitivity": {
"name": "Motion sensitivity"
},
"system_sounds_volume": {
"name": "System sounds volume"
},
"volume": {
"name": "[%key:component::sensor::entity_component::volume::name%]"
},
@@ -567,8 +582,26 @@
"api_key_required": {
"message": "API key is required. Please reauthenticate this integration to provide an API key."
},
"device_not_found": {
"message": "No device found for device id: {device_id}"
},
"no_users_found": {
"message": "No users found, please check Protect permissions"
},
"only_music_supported": {
"message": "Only music media type is supported"
},
"privacy_zone_not_found": {
"message": "Could not find privacy zone with name {zone_name} on camera {camera_name}"
},
"protect_version": {
"message": "Your UniFi Protect version ({current_version}) is too old. Minimum required: {min_version}."
"message": "Your UniFi Protect version ({current_version}) is too old. Minimum required: {min_version}"
},
"service_error": {
"message": "Error calling UniFi Protect service, check the logs for more details"
},
"stream_error": {
"message": "Error playing audio, check the logs for more details"
}
},
"issues": {
@@ -627,6 +660,12 @@
"max_media": "Max number of event to load for Media Browser (increases RAM usage)",
"override_connection_host": "Override connection host"
},
"data_description": {
"all_updates": "Enable realtime metrics updates. Only use if you have enabled diagnostic sensors and want them updated in realtime.",
"disable_rtsp": "Disable the RTSP stream for all cameras. Use this if you don't need live video feeds.",
"max_media": "Maximum number of events to load in the Media Browser. Higher values use more RAM.",
"override_connection_host": "Override the connection host for the UniFi Protect device."
},
"description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If not enabled, they will only update once every 15 minutes.",
"title": "UniFi Protect options"
}

View File

@@ -36,6 +36,7 @@ from .entity import (
ATTR_PREV_MIC = "prev_mic_level"
ATTR_PREV_RECORD = "prev_record_mode"
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)

View File

@@ -27,6 +27,8 @@ from .entity import (
async_all_device_entities,
)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class ProtectTextEntityDescription(ProtectSetableKeysMixin[T], TextEntityDescription):

View File

@@ -114,7 +114,6 @@
"triggers": {
"docked": {
"description": "Triggers when a vacuum cleaner has docked.",
"description_configured": "[%key:component::vacuum::triggers::docked::description%]",
"fields": {
"behavior": {
"description": "[%key:component::vacuum::common::trigger_behavior_description%]",
@@ -125,7 +124,6 @@
},
"errored": {
"description": "Triggers when a vacuum cleaner has errored.",
"description_configured": "[%key:component::vacuum::triggers::errored::description%]",
"fields": {
"behavior": {
"description": "[%key:component::vacuum::common::trigger_behavior_description%]",
@@ -136,7 +134,6 @@
},
"paused_cleaning": {
"description": "Triggers when a vacuum cleaner has paused cleaning.",
"description_configured": "[%key:component::vacuum::triggers::paused_cleaning::description%]",
"fields": {
"behavior": {
"description": "[%key:component::vacuum::common::trigger_behavior_description%]",
@@ -147,7 +144,6 @@
},
"started_cleaning": {
"description": "Triggers when a vacuum cleaner has started cleaning.",
"description_configured": "[%key:component::vacuum::triggers::started_cleaning::description%]",
"fields": {
"behavior": {
"description": "[%key:component::vacuum::common::trigger_behavior_description%]",

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/vesync",
"iot_class": "cloud_polling",
"loggers": ["pyvesync"],
"requirements": ["pyvesync==3.2.2"]
"requirements": ["pyvesync==3.3.2"]
}

View File

@@ -14,6 +14,7 @@ from homeassistant.core import (
ServiceResponse,
SupportsResponse,
)
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.location import find_coordinates
from homeassistant.helpers.selector import (
BooleanSelector,
@@ -46,7 +47,6 @@ from .const import (
VEHICLE_TYPES,
)
from .coordinator import WazeTravelTimeCoordinator, async_get_travel_times
from .httpx_client import create_httpx_client
PLATFORMS = [Platform.SENSOR]
@@ -106,8 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
if SEMAPHORE not in hass.data.setdefault(DOMAIN, {}):
hass.data.setdefault(DOMAIN, {})[SEMAPHORE] = asyncio.Semaphore(1)
httpx_client = await create_httpx_client(hass)
httpx_client = get_async_client(hass)
client = WazeRouteCalculator(
region=config_entry.data[CONF_REGION].upper(), client=httpx_client
)
@@ -120,7 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
async def async_get_travel_times_service(service: ServiceCall) -> ServiceResponse:
httpx_client = await create_httpx_client(hass)
httpx_client = get_async_client(hass)
client = WazeRouteCalculator(
region=service.data[CONF_REGION].upper(), client=httpx_client
)

View File

@@ -1,26 +0,0 @@
"""Special httpx client for Waze Travel Time integration."""
import httpx
from homeassistant.core import HomeAssistant
from homeassistant.helpers.httpx_client import create_async_httpx_client
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN
DATA_HTTPX_ASYNC_CLIENT: HassKey[httpx.AsyncClient] = HassKey("httpx_async_client")
def create_transport() -> httpx.AsyncHTTPTransport:
"""Create a httpx transport which enforces the use of IPv4."""
return httpx.AsyncHTTPTransport(local_address="0.0.0.0")
async def create_httpx_client(hass: HomeAssistant) -> httpx.AsyncClient:
"""Create a httpx client which enforces the use of IPv4."""
if (client := hass.data[DOMAIN].get(DATA_HTTPX_ASYNC_CLIENT)) is None:
transport = await hass.async_add_executor_job(create_transport)
client = hass.data[DOMAIN][DATA_HTTPX_ASYNC_CLIENT] = create_async_httpx_client(
hass, transport=transport
)
return client

View File

@@ -1553,7 +1553,9 @@ class Entity(
# Clear the remove future to handle entity added again after entity id change
self.__remove_future = None
self._platform_state = EntityPlatformState.NOT_ADDED
await self.platform.async_add_entities([self])
await self.platform.async_add_entities(
[self], config_subentry_id=registry_entry.config_subentry_id
)
@callback
def _async_unsubscribe_device_updates(self) -> None:

22
requirements_all.txt generated
View File

@@ -252,7 +252,7 @@ aioelectricitymaps==1.1.1
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==42.8.0
aioesphomeapi==42.9.0
# homeassistant.components.matrix
# homeassistant.components.slack
@@ -393,7 +393,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0
# homeassistant.components.shelly
aioshelly==13.21.0
aioshelly==13.22.0
# homeassistant.components.skybell
aioskybell==22.7.0
@@ -504,7 +504,7 @@ anova-wifi==0.17.0
anthemav==1.4.1
# homeassistant.components.anthropic
anthropic==0.73.0
anthropic==0.75.0
# homeassistant.components.mcp_server
anyio==4.10.0
@@ -675,7 +675,7 @@ bluetooth-data-tools==1.28.4
bond-async==0.2.1
# homeassistant.components.bosch_alarm
bosch-alarm-mode2==0.4.6
bosch-alarm-mode2==0.4.10
# homeassistant.components.bosch_shc
boschshcpy==0.2.107
@@ -1087,13 +1087,13 @@ google-genai==1.38.0
google-maps-routing==0.6.15
# homeassistant.components.nest
google-nest-sdm==9.1.0
google-nest-sdm==9.1.1
# homeassistant.components.google_photos
google-photos-library-api==0.12.1
# homeassistant.components.google_air_quality
google_air_quality_api==1.1.2
google_air_quality_api==1.1.3
# homeassistant.components.slide
# homeassistant.components.slide_local
@@ -2551,7 +2551,7 @@ python-overseerr==0.7.1
python-picnic-api2==1.3.1
# homeassistant.components.pooldose
python-pooldose==0.8.0
python-pooldose==0.8.1
# homeassistant.components.rabbitair
python-rabbitair==0.0.8
@@ -2560,7 +2560,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==3.8.3
python-roborock==3.8.4
# homeassistant.components.smarttub
python-smarttub==0.0.45
@@ -2630,7 +2630,7 @@ pyvera==0.3.16
pyversasense==0.0.6
# homeassistant.components.vesync
pyvesync==3.2.2
pyvesync==3.3.2
# homeassistant.components.vizio
pyvizio==0.1.61
@@ -2987,7 +2987,7 @@ thermopro-ble==1.1.2
thingspeak==1.0.0
# homeassistant.components.lg_thinq
thinqconnect==1.0.8
thinqconnect==1.0.9
# homeassistant.components.tikteck
tikteck==0.4
@@ -3053,7 +3053,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==7.29.0
uiprotect==7.31.0
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7

View File

@@ -243,7 +243,7 @@ aioelectricitymaps==1.1.1
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==42.8.0
aioesphomeapi==42.9.0
# homeassistant.components.matrix
# homeassistant.components.slack
@@ -378,7 +378,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0
# homeassistant.components.shelly
aioshelly==13.21.0
aioshelly==13.22.0
# homeassistant.components.skybell
aioskybell==22.7.0
@@ -480,7 +480,7 @@ anova-wifi==0.17.0
anthemav==1.4.1
# homeassistant.components.anthropic
anthropic==0.73.0
anthropic==0.75.0
# homeassistant.components.mcp_server
anyio==4.10.0
@@ -609,7 +609,7 @@ bluetooth-data-tools==1.28.4
bond-async==0.2.1
# homeassistant.components.bosch_alarm
bosch-alarm-mode2==0.4.6
bosch-alarm-mode2==0.4.10
# homeassistant.components.bosch_shc
boschshcpy==0.2.107
@@ -963,13 +963,13 @@ google-genai==1.38.0
google-maps-routing==0.6.15
# homeassistant.components.nest
google-nest-sdm==9.1.0
google-nest-sdm==9.1.1
# homeassistant.components.google_photos
google-photos-library-api==0.12.1
# homeassistant.components.google_air_quality
google_air_quality_api==1.1.2
google_air_quality_api==1.1.3
# homeassistant.components.slide
# homeassistant.components.slide_local
@@ -2132,13 +2132,13 @@ python-overseerr==0.7.1
python-picnic-api2==1.3.1
# homeassistant.components.pooldose
python-pooldose==0.8.0
python-pooldose==0.8.1
# homeassistant.components.rabbitair
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==3.8.3
python-roborock==3.8.4
# homeassistant.components.smarttub
python-smarttub==0.0.45
@@ -2196,7 +2196,7 @@ pyuptimerobot==22.2.0
pyvera==0.3.16
# homeassistant.components.vesync
pyvesync==3.2.2
pyvesync==3.3.2
# homeassistant.components.vizio
pyvizio==0.1.61
@@ -2481,7 +2481,7 @@ thermobeacon-ble==0.10.0
thermopro-ble==1.1.2
# homeassistant.components.lg_thinq
thinqconnect==1.0.8
thinqconnect==1.0.9
# homeassistant.components.tilt_ble
tilt-ble==1.0.1
@@ -2538,7 +2538,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==7.29.0
uiprotect==7.31.0
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7

View File

@@ -475,7 +475,6 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
{
vol.Required("name"): translation_value_validator,
vol.Required("description"): translation_value_validator,
vol.Required("description_configured"): translation_value_validator,
vol.Optional("fields"): cv.schema_with_slug_keys(
{
vol.Required("name"): str,
@@ -491,7 +490,6 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
{
vol.Required("name"): translation_value_validator,
vol.Required("description"): translation_value_validator,
vol.Required("description_configured"): translation_value_validator,
vol.Optional("fields"): cv.schema_with_slug_keys(
{
vol.Required("name"): str,

View File

@@ -128,6 +128,12 @@ async def mock_init_component(
"""Initialize integration."""
model_list = AsyncPage(
data=[
ModelInfo(
id="claude-opus-4-5-20251101",
created_at=datetime.datetime(2025, 11, 1, 0, 0, tzinfo=datetime.UTC),
display_name="Claude Opus 4.5",
type="model",
),
ModelInfo(
id="claude-haiku-4-5-20251001",
created_at=datetime.datetime(2025, 10, 15, 0, 0, tzinfo=datetime.UTC),

View File

@@ -357,6 +357,10 @@ async def test_model_list(
assert options["type"] == FlowResultType.FORM
assert options["step_id"] == "advanced"
assert options["data_schema"].schema["chat_model"].config["options"] == [
{
"label": "Claude Opus 4.5",
"value": "claude-opus-4-5",
},
{
"label": "Claude Haiku 4.5",
"value": "claude-haiku-4-5",
@@ -379,11 +383,11 @@ async def test_model_list(
},
{
"label": "Claude Sonnet 3.7",
"value": "claude-3-7-sonnet",
"value": "claude-3-7-sonnet-latest",
},
{
"label": "Claude Haiku 3.5",
"value": "claude-3-5-haiku",
"value": "claude-3-5-haiku-latest",
},
{
"label": "Claude Haiku 3",

View File

@@ -4,6 +4,7 @@ from collections.abc import Generator
from typing import Any
from unittest.mock import AsyncMock, patch
from bosch_alarm_mode2.const import PANEL_FAMILY, PanelModel
from bosch_alarm_mode2.panel import Area, Door, Output, Point
from bosch_alarm_mode2.utils import Observable
import pytest
@@ -39,10 +40,10 @@ def model(request: pytest.FixtureRequest) -> Generator[str]:
@pytest.fixture
def extra_config_entry_data(
model: str, model_name: str, config_flow_data: dict[str, Any]
model: str, panel_model: PanelModel, config_flow_data: dict[str, Any]
) -> dict[str, Any]:
"""Return extra config entry data."""
return {CONF_MODEL: model_name} | config_flow_data
return {CONF_MODEL: panel_model.name} | config_flow_data
@pytest.fixture(params=[None])
@@ -64,12 +65,12 @@ def config_flow_data(model: str) -> dict[str, Any]:
@pytest.fixture
def model_name(model: str) -> str | None:
def panel_model(model: str) -> PanelModel | None:
"""Return extra config entry data."""
return {
"solution_3000": "Solution 3000",
"amax_3000": "AMAX 3000",
"b5512": "B5512 (US1B)",
"solution_3000": PanelModel("Solution 3000", PANEL_FAMILY.SOLUTION),
"amax_3000": PanelModel("AMAX 3000", PANEL_FAMILY.AMAX),
"b5512": PanelModel("B5512 (US1B)", PANEL_FAMILY.BG_SERIES),
}.get(model)
@@ -166,7 +167,7 @@ def mock_panel(
door: AsyncMock,
output: AsyncMock,
points: dict[int, AsyncMock],
model_name: str,
panel_model: str,
serial_number: str | None,
) -> Generator[AsyncMock]:
"""Define a fixture to set up Bosch Alarm."""
@@ -181,7 +182,7 @@ def mock_panel(
client.doors = {1: door}
client.outputs = {1: output}
client.points = points
client.model = model_name
client.model = panel_model
client.faults = []
client.events = []
client.panel_faults_ids = []

View File

@@ -28,6 +28,7 @@
'open': False,
}),
]),
'family': 'AMAX',
'firmware_version': '1.0.0',
'history_events': list([
]),
@@ -124,6 +125,7 @@
'open': False,
}),
]),
'family': 'BG_SERIES',
'firmware_version': '1.0.0',
'history_events': list([
]),
@@ -219,6 +221,7 @@
'open': False,
}),
]),
'family': 'SOLUTION',
'firmware_version': '1.0.0',
'history_events': list([
]),

View File

@@ -4,6 +4,7 @@ import asyncio
from typing import Any
from unittest.mock import AsyncMock
from bosch_alarm_mode2.const import PANEL_FAMILY, PanelModel
import pytest
from homeassistant.components.bosch_alarm.const import DOMAIN
@@ -22,7 +23,7 @@ async def test_form_user(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -45,13 +46,13 @@ async def test_form_user(
config_flow_data,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"Bosch {model_name}"
assert result["title"] == f"Bosch {panel_model.name}"
assert (
result["data"]
== {
CONF_HOST: "1.1.1.1",
CONF_PORT: 7700,
CONF_MODEL: model_name,
CONF_MODEL: panel_model.name,
}
| config_flow_data
)
@@ -211,7 +212,7 @@ async def test_dhcp_can_finish(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -237,12 +238,12 @@ async def test_dhcp_can_finish(
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"Bosch {model_name}"
assert result["title"] == f"Bosch {panel_model.name}"
assert result["data"] == {
CONF_HOST: "1.1.1.1",
CONF_MAC: "34:ea:34:b4:3b:5a",
CONF_PORT: 7700,
CONF_MODEL: model_name,
CONF_MODEL: panel_model.name,
**config_flow_data,
}
@@ -258,7 +259,7 @@ async def test_dhcp_exceptions(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
exception: Exception,
@@ -316,7 +317,7 @@ async def test_dhcp_discovery_if_panel_setup_config_flow(
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
serial_number: str,
model_name: str,
panel_model: PanelModel,
config_flow_data: dict[str, Any],
) -> None:
"""Test DHCP discovery doesn't fail if a different panel was set up via config flow."""
@@ -346,12 +347,12 @@ async def test_dhcp_discovery_if_panel_setup_config_flow(
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"Bosch {model_name}"
assert result["title"] == f"Bosch {panel_model.name}"
assert result["data"] == {
CONF_HOST: "4.5.6.7",
CONF_MAC: "34:ea:34:b4:3b:5a",
CONF_PORT: 7700,
CONF_MODEL: model_name,
CONF_MODEL: panel_model.name,
**config_flow_data,
}
assert mock_config_entry.unique_id == serial_number
@@ -395,7 +396,7 @@ async def test_dhcp_updates_mac(
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -424,7 +425,7 @@ async def test_reauth_flow_success(
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -459,7 +460,7 @@ async def test_reauth_flow_error(
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
exception: Exception,
@@ -494,7 +495,7 @@ async def test_reconfig_flow(
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -529,7 +530,7 @@ async def test_reconfig_flow(
assert mock_config_entry.data == {
CONF_HOST: "1.1.1.1",
CONF_PORT: 7700,
CONF_MODEL: model_name,
CONF_MODEL: panel_model.name,
**config_flow_data,
}
@@ -540,7 +541,7 @@ async def test_reconfig_flow_incorrect_model(
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
@@ -556,7 +557,7 @@ async def test_reconfig_flow_incorrect_model(
},
)
mock_panel.model = "Solution 3000"
mock_panel.model = PanelModel("Solution 3000", family=PANEL_FAMILY.SOLUTION)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"

View File

@@ -3,6 +3,7 @@
from typing import Any
from unittest.mock import AsyncMock
from bosch_alarm_mode2.const import PanelModel
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
@@ -19,7 +20,7 @@ async def test_diagnostics(
hass_client: ClientSessionGenerator,
mock_panel: AsyncMock,
area: AsyncMock,
model_name: str,
panel_model: PanelModel,
serial_number: str,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,

View File

@@ -1,297 +0,0 @@
"""Test cover trigger."""
from collections.abc import Generator
from unittest.mock import patch
import pytest
from homeassistant.components.cover import ATTR_CURRENT_POSITION, CoverState
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_LABEL_ID, CONF_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.setup import async_setup_component
from tests.components import (
StateDescription,
arm_trigger,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture(name="enable_experimental_triggers_conditions")
def enable_experimental_triggers_conditions() -> Generator[None]:
"""Enable experimental triggers and conditions."""
with patch(
"homeassistant.components.labs.async_is_preview_feature_enabled",
return_value=True,
):
yield
@pytest.fixture
async def target_covers(hass: HomeAssistant) -> list[str]:
"""Create multiple cover entities associated with different targets."""
return await target_entities(hass, "cover")
def parametrize_opened_trigger_states(
trigger: str, device_class: str
) -> list[tuple[str, dict, str, list[StateDescription]]]:
"""Parametrize states and expected service call counts.
Returns a list of tuples with (trigger, trigger_options,
list of StateDescription).
"""
additional_attributes = {ATTR_DEVICE_CLASS: device_class}
return [
# Test fully_opened = True
*(
(s[0], {"fully_opened": True}, *s[1:])
for s in parametrize_trigger_states(
trigger=trigger,
target_states=[
(CoverState.OPEN, {}),
(CoverState.OPENING, {}),
(CoverState.OPEN, {ATTR_CURRENT_POSITION: 100}),
(CoverState.OPENING, {ATTR_CURRENT_POSITION: 100}),
],
other_states=[
(CoverState.CLOSED, {}),
(CoverState.OPEN, {ATTR_CURRENT_POSITION: 0}),
],
additional_attributes=additional_attributes,
trigger_from_none=False,
)
),
# Test fully_opened = False
*(
(s[0], {}, *s[1:])
for s in parametrize_trigger_states(
trigger=trigger,
target_states=[
(CoverState.OPEN, {}),
(CoverState.OPENING, {}),
(CoverState.OPEN, {ATTR_CURRENT_POSITION: 1}),
(CoverState.OPENING, {ATTR_CURRENT_POSITION: 1}),
],
other_states=[
(CoverState.CLOSED, {}),
(CoverState.CLOSED, {ATTR_CURRENT_POSITION: 0}),
],
additional_attributes=additional_attributes,
trigger_from_none=False,
)
),
]
@pytest.mark.parametrize(
"trigger_key",
[
"cover.awning_opened",
"cover.blind_opened",
"cover.curtain_opened",
"cover.door_opened",
"cover.garage_opened",
"cover.gate_opened",
"cover.shade_opened",
"cover.shutter_opened",
"cover.window_opened",
],
)
async def test_cover_triggers_gated_by_labs_flag(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
) -> None:
"""Test the cover triggers are gated by the labs flag."""
await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"})
assert (
"Unnamed automation failed to setup triggers and has been disabled: Trigger "
f"'{trigger_key}' requires the experimental 'New triggers and conditions' "
"feature to be enabled in Home Assistant Labs settings (feature flag: "
"'new_triggers_conditions')"
) in caplog.text
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("cover"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_opened_trigger_states("cover.awning_opened", "awning"),
*parametrize_opened_trigger_states("cover.blind_opened", "blind"),
*parametrize_opened_trigger_states("cover.curtain_opened", "curtain"),
*parametrize_opened_trigger_states("cover.door_opened", "door"),
*parametrize_opened_trigger_states("cover.garage_opened", "garage"),
*parametrize_opened_trigger_states("cover.gate_opened", "gate"),
*parametrize_opened_trigger_states("cover.shade_opened", "shade"),
*parametrize_opened_trigger_states("cover.shutter_opened", "shutter"),
*parametrize_opened_trigger_states("cover.window_opened", "window"),
],
)
async def test_cover_state_attribute_trigger_behavior_any(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_covers: list[str],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict,
states: list[StateDescription],
) -> None:
"""Test that the cover state trigger fires when any cover state changes to a specific state."""
await async_setup_component(hass, "cover", {})
other_entity_ids = set(target_covers) - {entity_id}
# Set all covers, including the tested cover, to the initial state
for eid in target_covers:
set_or_remove_state(hass, eid, states[0])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, trigger_options, trigger_target_config)
for state in states[1:]:
set_or_remove_state(hass, entity_id, state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
# Check if changing other covers also triggers
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, state)
await hass.async_block_till_done()
assert len(service_calls) == (entities_in_target - 1) * state["count"]
service_calls.clear()
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("cover"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_opened_trigger_states("cover.awning_opened", "awning"),
*parametrize_opened_trigger_states("cover.blind_opened", "blind"),
*parametrize_opened_trigger_states("cover.curtain_opened", "curtain"),
*parametrize_opened_trigger_states("cover.door_opened", "door"),
*parametrize_opened_trigger_states("cover.garage_opened", "garage"),
*parametrize_opened_trigger_states("cover.gate_opened", "gate"),
*parametrize_opened_trigger_states("cover.shade_opened", "shade"),
*parametrize_opened_trigger_states("cover.shutter_opened", "shutter"),
*parametrize_opened_trigger_states("cover.window_opened", "window"),
],
)
async def test_cover_state_attribute_trigger_behavior_first(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_covers: list[str],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict,
states: list[StateDescription],
) -> None:
"""Test that the cover state trigger fires when the first cover state changes to a specific state."""
await async_setup_component(hass, "cover", {})
other_entity_ids = set(target_covers) - {entity_id}
# Set all covers, including the tested cover, to the initial state
for eid in target_covers:
set_or_remove_state(hass, eid, states[0])
await hass.async_block_till_done()
await arm_trigger(
hass,
trigger,
{"behavior": "first"} | trigger_options,
trigger_target_config,
)
for state in states[1:]:
set_or_remove_state(hass, entity_id, state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
# Triggering other covers should not cause the trigger to fire again
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, state)
await hass.async_block_till_done()
assert len(service_calls) == 0
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("cover"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_opened_trigger_states("cover.awning_opened", "awning"),
*parametrize_opened_trigger_states("cover.blind_opened", "blind"),
*parametrize_opened_trigger_states("cover.curtain_opened", "curtain"),
*parametrize_opened_trigger_states("cover.door_opened", "door"),
*parametrize_opened_trigger_states("cover.garage_opened", "garage"),
*parametrize_opened_trigger_states("cover.gate_opened", "gate"),
*parametrize_opened_trigger_states("cover.shade_opened", "shade"),
*parametrize_opened_trigger_states("cover.shutter_opened", "shutter"),
*parametrize_opened_trigger_states("cover.window_opened", "window"),
],
)
async def test_cover_state_attribute_trigger_behavior_last(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_covers: list[str],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict,
states: list[StateDescription],
) -> None:
"""Test that the cover state trigger fires when the last cover state changes to a specific state."""
await async_setup_component(hass, "cover", {})
other_entity_ids = set(target_covers) - {entity_id}
# Set all covers, including the tested cover, to the initial state
for eid in target_covers:
set_or_remove_state(hass, eid, states[0])
await hass.async_block_till_done()
await arm_trigger(
hass, trigger, {"behavior": "last"} | trigger_options, trigger_target_config
)
for state in states[1:]:
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()

View File

@@ -91,6 +91,7 @@ async def integration_fixture(
"door_lock_with_unbolt",
"eberle_ute3000",
"ecovacs_deebot",
"eufy_vacuum_omni_e28",
"eve_contact_sensor",
"eve_energy_20ecn4101",
"eve_energy_plug",
@@ -113,6 +114,7 @@ async def integration_fixture(
"light_sensor",
"microwave_oven",
"mock_lock",
"mock_thermostat",
"mounted_dimmable_load_control_fixture",
"multi_endpoint_light",
"occupancy_sensor",

View File

@@ -0,0 +1,652 @@
{
"node_id": 40,
"date_commissioned": "2025-11-27T01:47:55.787124",
"last_interview": "2025-11-27T01:49:41.087618",
"interview_version": 6,
"available": true,
"is_bridge": false,
"attributes": {
"0/3/0": 0,
"0/3/1": 2,
"0/3/65532": 0,
"0/3/65533": 5,
"0/3/65528": [],
"0/3/65529": [0],
"0/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"0/29/0": [
{
"0": 22,
"1": 1
}
],
"0/29/1": [3, 29, 31, 40, 46, 48, 51, 60, 62, 63],
"0/29/2": [],
"0/29/3": [1],
"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": [
{
"254": 5
},
{
"254": 5
},
{
"254": 6
},
{
"1": 5,
"2": 2,
"3": [112233],
"4": null,
"254": 7
}
],
"0/31/2": 4,
"0/31/3": 3,
"0/31/4": 4,
"0/31/65532": 0,
"0/31/65533": 2,
"0/31/65528": [],
"0/31/65529": [],
"0/31/65531": [0, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/40/0": 18,
"0/40/1": "Anker Innovations Technology",
"0/40/2": 5427,
"0/40/3": "eufy Robot Vacuum Omni E28",
"0/40/4": 10,
"0/40/5": "",
"0/40/6": "**REDACTED**",
"0/40/7": 7,
"0/40/8": "V7",
"0/40/9": 3,
"0/40/10": "3.0",
"0/40/11": "20250107",
"0/40/13": "https://www.eufy.com/collections/robot-vacuum-with-docking-station?ref=navimenu_1_2_2_all_copy",
"0/40/14": "2BAVS-AB6031X-44PE",
"0/40/15": "*********************",
"0/40/16": false,
"0/40/18": "*********************",
"0/40/19": {
"0": 3,
"1": 65535
},
"0/40/21": 17039360,
"0/40/22": 1,
"0/40/65532": 0,
"0/40/65533": 4,
"0/40/65528": [],
"0/40/65529": [],
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 18, 19, 21, 22,
65528, 65529, 65531, 65532, 65533
],
"0/46/0": [1],
"0/46/65532": 0,
"0/46/65533": 1,
"0/46/65528": [],
"0/46/65529": [],
"0/46/65531": [0, 65528, 65529, 65531, 65532, 65533],
"0/48/0": 0,
"0/48/1": {
"0": 60,
"1": 900
},
"0/48/2": 0,
"0/48/3": 2,
"0/48/4": true,
"0/48/65532": 0,
"0/48/65533": 2,
"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/51/0": [
{
"0": "p2p0",
"1": false,
"2": null,
"3": null,
"4": "Mr4pWXYJ",
"5": ["wKivAQ=="],
"6": [],
"7": 1
},
{
"0": "wlan0",
"1": true,
"2": null,
"3": null,
"4": "ML4pWXYJ",
"5": ["wKgUKQ=="],
"6": ["/oAAAAAAAADnm2WwWr92vA=="],
"7": 1
},
{
"0": "sit0",
"1": false,
"2": null,
"3": null,
"4": "AAAAAAAA",
"5": [],
"6": [],
"7": 0
},
{
"0": "lo",
"1": true,
"2": null,
"3": null,
"4": "AAAAAAAA",
"5": ["fwAAAQ=="],
"6": ["AAAAAAAAAAAAAAAAAAAAAQ=="],
"7": 0
}
],
"0/51/1": 41,
"0/51/2": 159947,
"0/51/3": 3133,
"0/51/4": 1,
"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/60/0": 0,
"0/60/1": null,
"0/60/2": null,
"0/60/65532": 0,
"0/60/65533": 1,
"0/60/65528": [],
"0/60/65529": [0, 2],
"0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
"0/62/0": [
{
"254": 5
},
{
"254": 6
},
{
"1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRKBgkBwEkCAEwCUEEzo04tfIUWFsjmoJzC0tgzeNlmT2cF6jmbDUuiF5q9X4b2kR9IFHTZnvd/gxTq538148FD9yBam9FTVotQe8wVDcKNQEoARgkAgE2AwQCBAEYMAQUKoF/1myXOXBAGIBagkz+UR460bQwBRRwoL/FqInZqSh+z7QAK0r1ew8axhgwC0DlvLjZ5VajnaTLnKa2JsKRrjratV6Kr0Ip3vkNck4zW9vpKVoHj3LKptNBQ4ROHaZX6eZNOZXXn3ikvKKo/pzGGA==",
"2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE2El8wzEcEHk4un8ZJSR8RWH86lcOvI5Q5bfSmDEaoEOyvYLA4Td/T0i7ZFpv9X0+CFDOgK9vV4sZ7sN8MSxMsTcKNQEpARgkAmAwBBRwoL/FqInZqSh+z7QAK0r1ew8axjAFFHGq+OWnV3HomxvqSHDxukU8THLXGDALQBMwck3ryDmue0Fx8kGCNAA8SjFWZF1kh+JWaesBrNwcO3oV7dEIvAblOuMmnolx5Wf/5Hbp8YRjEzKxnFyddR0Y",
"254": 7
}
],
"0/62/1": [
{
"1": "BFhpm8fVgw4hzcuwFGwSe59XhvdUHtMntaUUbgCX0jqoaA1fjjcRYrZCA0PDImdLtZSkrUdug3S/euAVf4gvaKo=",
"2": 4937,
"3": 3672061662,
"4": 4176574991,
"5": "JVChewy",
"254": 5
},
{
"1": "BP+6ik8SJgoRwv2p99/fGYj5G4xjjiyCNSGM83kGa6FxiLEceE4RbQar+xbksBwnzBqmqYA7PwwIZ8jdL5zKSLs=",
"2": 4996,
"3": 2560414293,
"4": 3062529239,
"5": "",
"254": 6
},
{
"1": "BC59F5uD+hRhy6qCXVp+pMpuBx68wgc6eTF57OLWHJ3+Wq1/bs3OLFr2vh4vCBzvG7C0Ijo6yKK4JERyWEtFS1E=",
"2": 4939,
"3": 2,
"4": 40,
"5": "JV Home",
"254": 7
}
],
"0/62/2": 16,
"0/62/3": 3,
"0/62/4": [
"FTABAQAkAgE3AyYUWi+aACYV3jbf2hgmBORpujAkBQA3BiYUWi+aACYV3jbf2hgkBwEkCAEwCUEEWGmbx9WDDiHNy7AUbBJ7n1eG91Qe0ye1pRRuAJfSOqhoDV+ONxFitkIDQ8MiZ0u1lKStR26DdL964BV/iC9oqjcKNQEpARgkAmAwBBSE8GakJIDzzseC1x/uiMKWPWSNSjAFFITwZqQkgPPOx4LXH+6IwpY9ZI1KGDALQB2kDCqAOX3YSsiOMWzA3Zq43VKqux6XQb3UtoELJok73SJIjo9YJ5UQ9bMIQIDZkW2ckuD2t4oGKSwY8tnsTwAY",
"FTABAQAkAgE3AycU7bEVXp1kzNEmFVXSnJgYJgR5MYMuJAUANwYnFO2xFV6dZMzRJhVV0pyYGCQHASQIATAJQQT/uopPEiYKEcL9qfff3xmI+RuMY44sgjUhjPN5BmuhcYixHHhOEW0Gq/sW5LAcJ8wapqmAOz8MCGfI3S+cyki7Nwo1ASkBGCQCYDAEFE18X69qbhhbH5f5+ExXr4ahWpvBMAUUTXxfr2puGFsfl/n4TFevhqFam8EYMAtAOhpZE0/FxSRhumv4BbBDb2ogpiwzr7lrC8FBk5BZxn2L0yNLqxTDNiOPuqv3eYgsISs3KyMDhvI05izsgLaHqhg=",
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEELn0Xm4P6FGHLqoJdWn6kym4HHrzCBzp5MXns4tYcnf5arX9uzc4sWva+Hi8IHO8bsLQiOjrIorgkRHJYS0VLUTcKNQEpARgkAmAwBBRxqvjlp1dx6Jsb6khw8bpFPExy1zAFFHGq+OWnV3HomxvqSHDxukU8THLXGDALQEk9bbDTdpQroquObcShGBnZjzzXHVDRWxMp7o3i977ujuEdGiVOZVKQxNDO2wh6S7mEglvWP4qw4oE1+AEEPCgY"
],
"0/62/5": 7,
"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],
"1/3/0": 0,
"1/3/1": 2,
"1/3/65532": 0,
"1/3/65533": 5,
"1/3/65528": [],
"1/3/65529": [0],
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/29/0": [
{
"0": 17,
"1": 1
},
{
"0": 116,
"1": 1
}
],
"1/29/1": [3, 29, 47, 84, 85, 97, 336],
"1/29/2": [],
"1/29/3": [],
"1/29/65532": 0,
"1/29/65533": 2,
"1/29/65528": [],
"1/29/65529": [],
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/47/0": 1,
"1/47/1": 0,
"1/47/2": "Battery",
"1/47/12": 200,
"1/47/14": 0,
"1/47/15": false,
"1/47/16": 0,
"1/47/26": 2,
"1/47/28": false,
"1/47/31": [],
"1/47/65532": 6,
"1/47/65533": 3,
"1/47/65528": [],
"1/47/65529": [],
"1/47/65531": [
0, 1, 2, 12, 14, 15, 16, 26, 28, 31, 65528, 65529, 65531, 65532, 65533
],
"1/84/0": [
{
"0": "Idle",
"1": 0,
"2": [
{
"1": 16384
}
]
},
{
"0": "Cleaning",
"1": 1,
"2": [
{
"1": 16385
}
]
},
{
"0": "Mapping",
"1": 2,
"2": [
{
"1": 16386
}
]
}
],
"1/84/1": 0,
"1/84/65532": 0,
"1/84/65533": 3,
"1/84/65528": [1],
"1/84/65529": [0],
"1/84/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/85/0": [
{
"0": "Auto Mop & Vacuum",
"1": 0,
"2": [
{
"1": 0
},
{
"1": 16386
},
{
"1": 16385
}
]
},
{
"0": "Auto Vacuum",
"1": 1,
"2": [
{
"1": 0
},
{
"1": 16385
}
]
},
{
"0": "Auto Deep Mop & Vacuum",
"1": 2,
"2": [
{
"1": 0
},
{
"1": 16384
},
{
"1": 16386
},
{
"1": 16385
}
]
},
{
"0": "Quick Mop & Vacuum",
"1": 3,
"2": [
{
"1": 1
},
{
"1": 16386
},
{
"1": 16385
}
]
},
{
"0": "Quick Vacuum",
"1": 4,
"2": [
{
"1": 1
},
{
"1": 16385
}
]
},
{
"0": "Low Noise Mop & Vacuum",
"1": 5,
"2": [
{
"1": 3
},
{
"1": 16386
},
{
"1": 16385
}
]
},
{
"0": "Low Noise Vacuum",
"1": 6,
"2": [
{
"1": 3
},
{
"1": 16385
}
]
},
{
"0": "Eufy Customize Mode",
"1": 7,
"2": [
{
"1": 0
}
]
}
],
"1/85/1": 0,
"1/85/65532": 65536,
"1/85/65533": 3,
"1/85/65528": [1],
"1/85/65529": [0],
"1/85/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/97/0": null,
"1/97/1": null,
"1/97/3": [
{
"0": 0
},
{
"0": 1
},
{
"0": 2
},
{
"0": 3
},
{
"0": 64
},
{
"0": 65
},
{
"0": 66
}
],
"1/97/4": 0,
"1/97/5": {
"0": 0
},
"1/97/65532": 0,
"1/97/65533": 2,
"1/97/65528": [4],
"1/97/65529": [0, 3, 128],
"1/97/65531": [0, 1, 3, 4, 5, 65528, 65529, 65531, 65532, 65533],
"1/336/0": [
{
"0": 40001,
"1": 4,
"2": {
"0": {
"0": "Dining Room",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40002,
"1": 4,
"2": {
"0": {
"0": "Spare Bedroom",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40003,
"1": 4,
"2": {
"0": {
"0": "Living Room",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40004,
"1": 4,
"2": {
"0": {
"0": "Kitchen",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40005,
"1": 4,
"2": {
"0": {
"0": "Master bedroom",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40006,
"1": 4,
"2": {
"0": {
"0": "Laundry Room",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40007,
"1": 4,
"2": {
"0": {
"0": "Master Closet",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40008,
"1": 4,
"2": {
"0": {
"0": "Back Hallway",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40009,
"1": 4,
"2": {
"0": {
"0": "Home Office",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40010,
"1": 4,
"2": {
"0": {
"0": "room_10",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40011,
"1": 4,
"2": {
"0": {
"0": "Gym",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40012,
"1": 4,
"2": {
"0": {
"0": "room_12",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40013,
"1": 4,
"2": {
"0": {
"0": "Master Bathroom",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40014,
"1": 4,
"2": {
"0": {
"0": "Front Hallway",
"1": null,
"2": null
},
"1": null
}
},
{
"0": 40015,
"1": 4,
"2": {
"0": {
"0": "Guest Bathroom",
"1": null,
"2": null
},
"1": null
}
}
],
"1/336/1": [
{
"0": 4,
"1": "map_4"
}
],
"1/336/2": [],
"1/336/3": null,
"1/336/5": [],
"1/336/65532": 6,
"1/336/65533": 1,
"1/336/65528": [1],
"1/336/65529": [0],
"1/336/65531": [0, 1, 2, 3, 5, 65528, 65529, 65531, 65532, 65533]
},
"attribute_subscriptions": []
}

View File

@@ -0,0 +1,526 @@
{
"node_id": 150,
"date_commissioned": "2025-11-18T06:53:08.679289",
"last_interview": "2025-11-18T06:53:08.679325",
"interview_version": 6,
"available": true,
"is_bridge": false,
"attributes": {
"0/49/0": 1,
"0/49/1": [
{
"0": "ZW5zMzM=",
"1": true
}
],
"0/49/4": true,
"0/49/5": 0,
"0/49/6": "ZW5zMzM=",
"0/49/7": null,
"0/49/65532": 4,
"0/49/65533": 2,
"0/49/65528": [],
"0/49/65529": [],
"0/49/65531": [0, 1, 4, 5, 6, 7, 65532, 65533, 65528, 65529, 65531],
"0/65/0": [],
"0/65/65532": 0,
"0/65/65533": 1,
"0/65/65528": [],
"0/65/65529": [],
"0/65/65531": [0, 65532, 65533, 65528, 65529, 65531],
"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": [5, 2],
"0/63/65529": [0, 1, 3, 4],
"0/63/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
"0/62/0": [
{
"1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRlhgkBwEkCAEwCUEE2p7AKvoklmZUFHB0JFUiCsv5FCm0dmeH35yXz4UUH4HAWUwpbeU+R7hMGbAITM3T1R/mVWYthssdVcPNsfIVcjcKNQEoARgkAgE2AwQCBAEYMAQUQbZ3toX8hpE/FmJz7M6xHTbh6RMwBRS5+zzv8ZPGnI9mC3wH9vq10JnwlhgwC0DughBITJJHW/pS7o0J6o6FYTe1ufe0vCpaCj3qYeWb/QxLUydUaJQbce5Z3lUcFeHybUa/M9HID+0PRp2Ker3/GA==",
"2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE/DujEcdTsX19xbxX+KuKKWiMaA5D9u99P/pVxIOmscd2BA2PadEMNnjvtPOpf+WE2Zxar4rby1IfAClGUUuQrTcKNQEpARgkAmAwBBS5+zzv8ZPGnI9mC3wH9vq10JnwljAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQGkPpvsbkAFEbfPN6H3Kf23R0zzmW/gpAA3kgaL6wKB2Ofm+Tmylw22qM536Kj8mOMwaV0EL1dCCGcuxF98aL6gY",
"254": 1
}
],
"0/62/1": [
{
"1": "BBmX+KwLR5HGlVNbvlC+dO8Jv9fPthHiTfGpUzi2JJADX5az6GxBAFn02QKHwLcZHyh+lh9faf6rf38/nPYF7/M=",
"2": 4939,
"3": 2,
"4": 150,
"5": "ha",
"254": 1
}
],
"0/62/2": 16,
"0/62/3": 1,
"0/62/4": [
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEGZf4rAtHkcaVU1u+UL507wm/18+2EeJN8alTOLYkkANflrPobEEAWfTZAofAtxkfKH6WH19p/qt/fz+c9gXv8zcKNQEpARgkAmAwBBT0+qfdyShnG+4Pq01pwOnrxdhHRjAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQPVrsFnfFplsQGV5m5EUua+rmo9hAr+OP1bvaifdLqiEIn3uXLTLoKmVUkPImRL2Fb+xcMEAqR2p7RM6ZlFCR20Y"
],
"0/62/5": 1,
"0/62/65532": 0,
"0/62/65533": 2,
"0/62/65528": [1, 3, 5, 8, 14],
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11, 12, 13],
"0/62/65531": [0, 1, 2, 3, 4, 5, 65532, 65533, 65528, 65529, 65531],
"0/60/0": 0,
"0/60/1": null,
"0/60/2": null,
"0/60/65532": 0,
"0/60/65533": 1,
"0/60/65528": [],
"0/60/65529": [0, 2],
"0/60/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531],
"0/55/2": 425,
"0/55/3": 61,
"0/55/4": 0,
"0/55/5": 0,
"0/55/6": 0,
"0/55/7": null,
"0/55/1": true,
"0/55/0": 2,
"0/55/8": 16,
"0/55/65532": 3,
"0/55/65533": 1,
"0/55/65528": [],
"0/55/65529": [0],
"0/55/65531": [
2, 3, 4, 5, 6, 7, 1, 0, 8, 65532, 65533, 65528, 65529, 65531
],
"0/54/0": null,
"0/54/1": null,
"0/54/2": 3,
"0/54/3": null,
"0/54/4": null,
"0/54/5": null,
"0/54/12": null,
"0/54/6": null,
"0/54/7": null,
"0/54/8": null,
"0/54/9": null,
"0/54/10": null,
"0/54/11": null,
"0/54/65532": 3,
"0/54/65533": 1,
"0/54/65528": [],
"0/54/65529": [0],
"0/54/65531": [
0, 1, 2, 3, 4, 5, 12, 6, 7, 8, 9, 10, 11, 65532, 65533, 65528, 65529,
65531
],
"0/52/0": [
{
"0": 6163,
"1": "6163"
},
{
"0": 6162,
"1": "6162"
},
{
"0": 6161,
"1": "6161"
},
{
"0": 6160,
"1": "6160"
},
{
"0": 6159,
"1": "6159"
}
],
"0/52/1": 545392,
"0/52/2": 650640,
"0/52/3": 650640,
"0/52/65532": 1,
"0/52/65533": 1,
"0/52/65528": [],
"0/52/65529": [0],
"0/52/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
"0/51/0": [
{
"0": "docker0",
"1": false,
"2": null,
"3": null,
"4": "8mJ0KirG",
"5": ["rBEAAQ=="],
"6": [],
"7": 0
},
{
"0": "ens33",
"1": true,
"2": null,
"3": null,
"4": "AAwpaqXN",
"5": ["wKgBxA=="],
"6": [
"KgEOCgKzOZAcmuLd4EsaUA==",
"KgEOCgKzOZA2wMm9YG06Ag==",
"/oAAAAAAAACluAo+qvkuxw=="
],
"7": 2
},
{
"0": "lo",
"1": true,
"2": null,
"3": null,
"4": "AAAAAAAA",
"5": ["fwAAAQ=="],
"6": ["AAAAAAAAAAAAAAAAAAAAAQ=="],
"7": 0
}
],
"0/51/1": 1,
"0/51/8": false,
"0/51/3": 0,
"0/51/4": 0,
"0/51/2": 16,
"0/51/65532": 0,
"0/51/65533": 2,
"0/51/65528": [2],
"0/51/65529": [0, 1],
"0/51/65531": [0, 1, 8, 3, 4, 2, 65532, 65533, 65528, 65529, 65531],
"0/50/65532": 0,
"0/50/65533": 1,
"0/50/65528": [1],
"0/50/65529": [0],
"0/50/65531": [65532, 65533, 65528, 65529, 65531],
"0/48/0": 0,
"0/48/1": {
"0": 60,
"1": 900
},
"0/48/2": 0,
"0/48/3": 2,
"0/48/4": true,
"0/48/65532": 0,
"0/48/65533": 2,
"0/48/65528": [1, 3, 5],
"0/48/65529": [0, 2, 4],
"0/48/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531],
"0/43/0": "en-US",
"0/43/1": ["en-US"],
"0/43/65532": 0,
"0/43/65533": 1,
"0/43/65528": [],
"0/43/65529": [],
"0/43/65531": [0, 1, 65532, 65533, 65528, 65529, 65531],
"0/40/0": 19,
"0/40/1": "TEST_VENDOR",
"0/40/2": 65521,
"0/40/3": "Mock Thermostat",
"0/40/4": 32769,
"0/40/5": "",
"0/40/6": "**REDACTED**",
"0/40/7": 0,
"0/40/8": "TEST_VERSION",
"0/40/9": 1,
"0/40/10": "1.0",
"0/40/19": {
"0": 3,
"1": 65535
},
"0/40/21": 17104896,
"0/40/22": 1,
"0/40/24": 1,
"0/40/11": "20200101",
"0/40/12": "",
"0/40/13": "",
"0/40/14": "",
"0/40/15": "TEST_SN",
"0/40/16": false,
"0/40/18": "29DB8B9DB518F05F",
"0/40/65532": 0,
"0/40/65533": 5,
"0/40/65528": [],
"0/40/65529": [],
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 19, 21, 22, 24, 11, 12, 13, 14, 15, 16,
18, 65532, 65533, 65528, 65529, 65531
],
"0/31/0": [
{
"1": 5,
"2": 2,
"3": [112233],
"4": null,
"254": 1
}
],
"0/31/2": 4,
"0/31/3": 3,
"0/31/4": 4,
"0/31/65532": 0,
"0/31/65533": 3,
"0/31/65528": [],
"0/31/65529": [],
"0/31/65531": [0, 2, 3, 4, 65532, 65533, 65528, 65529, 65531],
"0/30/0": [],
"0/30/65532": 0,
"0/30/65533": 1,
"0/30/65528": [],
"0/30/65529": [],
"0/30/65531": [0, 65532, 65533, 65528, 65529, 65531],
"0/29/0": [
{
"0": 18,
"1": 1
},
{
"0": 22,
"1": 3
}
],
"0/29/1": [
49, 65, 63, 62, 60, 55, 54, 52, 51, 50, 48, 43, 40, 31, 30, 29, 3, 42, 45,
53
],
"0/29/2": [41],
"0/29/3": [1],
"0/29/65532": 0,
"0/29/65533": 3,
"0/29/65528": [],
"0/29/65529": [],
"0/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
"0/3/0": 0,
"0/3/1": 2,
"0/3/65532": 0,
"0/3/65533": 6,
"0/3/65528": [],
"0/3/65529": [0, 64],
"0/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531],
"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, 65532, 65533, 65528, 65529, 65531],
"0/45/0": 1,
"0/45/65532": 1,
"0/45/65533": 2,
"0/45/65528": [],
"0/45/65529": [],
"0/45/65531": [0, 65532, 65533, 65528, 65529, 65531],
"0/53/0": null,
"0/53/1": null,
"0/53/2": null,
"0/53/3": null,
"0/53/4": null,
"0/53/5": null,
"0/53/6": 0,
"0/53/7": [],
"0/53/8": [],
"0/53/9": null,
"0/53/10": null,
"0/53/11": null,
"0/53/12": null,
"0/53/13": null,
"0/53/14": 0,
"0/53/15": 0,
"0/53/16": 0,
"0/53/17": 0,
"0/53/18": 0,
"0/53/19": 0,
"0/53/20": 0,
"0/53/21": 0,
"0/53/22": 0,
"0/53/23": 0,
"0/53/24": 0,
"0/53/25": 0,
"0/53/26": 0,
"0/53/27": 0,
"0/53/28": 0,
"0/53/29": 0,
"0/53/30": 0,
"0/53/31": 0,
"0/53/32": 0,
"0/53/33": 0,
"0/53/34": 0,
"0/53/35": 0,
"0/53/36": 0,
"0/53/37": 0,
"0/53/38": 0,
"0/53/39": 0,
"0/53/40": 0,
"0/53/41": 0,
"0/53/42": 0,
"0/53/43": 0,
"0/53/44": 0,
"0/53/45": 0,
"0/53/46": 0,
"0/53/47": 0,
"0/53/48": 0,
"0/53/49": 0,
"0/53/50": 0,
"0/53/51": 0,
"0/53/52": 0,
"0/53/53": 0,
"0/53/54": 0,
"0/53/55": 0,
"0/53/56": null,
"0/53/57": null,
"0/53/58": null,
"0/53/59": null,
"0/53/60": null,
"0/53/61": null,
"0/53/62": [],
"0/53/65532": 15,
"0/53/65533": 3,
"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, 65532, 65533, 65528, 65529, 65531
],
"1/29/0": [
{
"0": 769,
"1": 4
}
],
"1/29/1": [29, 3, 4, 513, 516],
"1/29/2": [3],
"1/29/3": [],
"1/29/65532": 0,
"1/29/65533": 3,
"1/29/65528": [],
"1/29/65529": [],
"1/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
"1/3/0": 0,
"1/3/1": 2,
"1/3/65532": 0,
"1/3/65533": 6,
"1/3/65528": [],
"1/3/65529": [0, 64],
"1/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531],
"1/4/0": 128,
"1/4/65532": 1,
"1/4/65533": 4,
"1/4/65528": [0, 1, 2, 3],
"1/4/65529": [0, 1, 2, 3, 4, 5],
"1/4/65531": [0, 65532, 65533, 65528, 65529, 65531],
"1/513/0": 1800,
"1/513/1": 500,
"1/513/3": 700,
"1/513/4": 3000,
"1/513/5": 1600,
"1/513/6": 3200,
"1/513/7": 0,
"1/513/8": 25,
"1/513/16": 0,
"1/513/17": 2600,
"1/513/18": 2000,
"1/513/21": 700,
"1/513/22": 3000,
"1/513/23": 1600,
"1/513/24": 3200,
"1/513/25": 25,
"1/513/26": 0,
"1/513/27": 4,
"1/513/28": 1,
"1/513/30": 4,
"1/513/35": 0,
"1/513/36": 0,
"1/513/37": 0,
"1/513/41": 1,
"1/513/48": 0,
"1/513/49": 150,
"1/513/50": 789004800,
"1/513/72": [
{
"0": 1,
"1": 1,
"2": 1
},
{
"0": 2,
"1": 1,
"2": 1
},
{
"0": 3,
"1": 1,
"2": 2
},
{
"0": 4,
"1": 1,
"2": 2
},
{
"0": 5,
"1": 1,
"2": 2
},
{
"0": 254,
"1": 1,
"2": 2
}
],
"1/513/73": [
{
"0": 4,
"1": 1,
"2": 2
},
{
"0": 3,
"1": 1,
"2": 2
}
],
"1/513/74": 5,
"1/513/78": null,
"1/513/80": [
{
"0": "AQ==",
"1": 1,
"3": 2500,
"4": 2100,
"5": true
},
{
"0": "Ag==",
"1": 2,
"3": 2600,
"4": 2000,
"5": true
}
],
"1/513/82": 0,
"1/513/83": 5,
"1/513/84": [],
"1/513/85": null,
"1/513/86": null,
"1/513/65532": 423,
"1/513/65533": 9,
"1/513/65528": [2, 253],
"1/513/65529": [0, 6, 7, 8, 254],
"1/513/65531": [
0, 1, 3, 4, 5, 6, 7, 8, 16, 17, 18, 21, 22, 23, 24, 25, 26, 27, 28, 30,
35, 36, 37, 41, 48, 49, 50, 72, 73, 74, 78, 80, 82, 83, 84, 85, 86, 65532,
65533, 65528, 65529, 65531
],
"1/516/0": 0,
"1/516/1": 0,
"1/516/65532": 0,
"1/516/65533": 2,
"1/516/65528": [],
"1/516/65529": [],
"1/516/65531": [0, 1, 65532, 65533, 65528, 65529, 65531]
},
"attribute_subscriptions": []
}

View File

@@ -391,6 +391,102 @@
'state': 'off',
})
# ---
# name: test_binary_sensors[eve_thermo][binary_sensor.eve_thermo_local_temperature_remote_sensing-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.eve_thermo_local_temperature_remote_sensing',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Local temperature remote sensing',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'thermostat_remote_sensing_local_temperature',
'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-ThermostatRemoteSensing_LocalTemperature-513-26',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[eve_thermo][binary_sensor.eve_thermo_local_temperature_remote_sensing-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Eve Thermo Local temperature remote sensing',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.eve_thermo_local_temperature_remote_sensing',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[eve_thermo][binary_sensor.eve_thermo_outdoor_temperature_remote_sensing-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.eve_thermo_outdoor_temperature_remote_sensing',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Outdoor temperature remote sensing',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'thermostat_remote_sensing_outdoor_temperature',
'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-ThermostatRemoteSensing_OutdoorTemperature-513-26',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[eve_thermo][binary_sensor.eve_thermo_outdoor_temperature_remote_sensing-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Eve Thermo Outdoor temperature remote sensing',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.eve_thermo_outdoor_temperature_remote_sensing',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[heiman_motion_sensor_m1][binary_sensor.smart_motion_sensor_occupancy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -636,6 +732,150 @@
'state': 'off',
})
# ---
# name: test_binary_sensors[mock_thermostat][binary_sensor.mock_thermostat_local_temperature_remote_sensing-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.mock_thermostat_local_temperature_remote_sensing',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Local temperature remote sensing',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'thermostat_remote_sensing_local_temperature',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatRemoteSensing_LocalTemperature-513-26',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[mock_thermostat][binary_sensor.mock_thermostat_local_temperature_remote_sensing-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Thermostat Local temperature remote sensing',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.mock_thermostat_local_temperature_remote_sensing',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[mock_thermostat][binary_sensor.mock_thermostat_occupancy_remote_sensing-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.mock_thermostat_occupancy_remote_sensing',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Occupancy remote sensing',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'thermostat_remote_sensing_occupancy',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatRemoteSensing_Occupancy-513-26',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[mock_thermostat][binary_sensor.mock_thermostat_occupancy_remote_sensing-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Thermostat Occupancy remote sensing',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.mock_thermostat_occupancy_remote_sensing',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[mock_thermostat][binary_sensor.mock_thermostat_outdoor_temperature_remote_sensing-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.mock_thermostat_outdoor_temperature_remote_sensing',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Outdoor temperature remote sensing',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'thermostat_remote_sensing_outdoor_temperature',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatRemoteSensing_OutdoorTemperature-513-26',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[mock_thermostat][binary_sensor.mock_thermostat_outdoor_temperature_remote_sensing-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Thermostat Outdoor temperature remote sensing',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.mock_thermostat_outdoor_temperature_remote_sensing',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[occupancy_sensor][binary_sensor.mock_occupancy_sensor_occupancy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -1026,6 +1026,104 @@
'state': 'unknown',
})
# ---
# name: test_buttons[eufy_vacuum_omni_e28][button.2bavs_ab6031x_44pe_identify_0-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.2bavs_ab6031x_44pe_identify_0',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (0)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000028-MatterNodeDevice-0-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[eufy_vacuum_omni_e28][button.2bavs_ab6031x_44pe_identify_0-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': '2BAVS-AB6031X-44PE Identify (0)',
}),
'context': <ANY>,
'entity_id': 'button.2bavs_ab6031x_44pe_identify_0',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[eufy_vacuum_omni_e28][button.2bavs_ab6031x_44pe_identify_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.2bavs_ab6031x_44pe_identify_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'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-0000000000000028-MatterNodeDevice-1-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[eufy_vacuum_omni_e28][button.2bavs_ab6031x_44pe_identify_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': '2BAVS-AB6031X-44PE Identify (1)',
}),
'context': <ANY>,
'entity_id': 'button.2bavs_ab6031x_44pe_identify_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[eve_contact_sensor][button.eve_door_identify-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -2290,6 +2388,104 @@
'state': 'unknown',
})
# ---
# name: test_buttons[mock_thermostat][button.mock_thermostat_identify_0-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.mock_thermostat_identify_0',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify (0)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-0-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[mock_thermostat][button.mock_thermostat_identify_0-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Mock Thermostat Identify (0)',
}),
'context': <ANY>,
'entity_id': 'button.mock_thermostat_identify_0',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[mock_thermostat][button.mock_thermostat_identify_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.mock_thermostat_identify_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'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-0000000000000096-MatterNodeDevice-1-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[mock_thermostat][button.mock_thermostat_identify_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Mock Thermostat Identify (1)',
}),
'context': <ANY>,
'entity_id': 'button.mock_thermostat_identify_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[multi_endpoint_light][button.inovelli_identify_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -325,6 +325,77 @@
'state': 'cool',
})
# ---
# name: test_climates[mock_thermostat][climate.mock_thermostat-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
]),
'max_temp': 32.0,
'min_temp': 7.0,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.mock_thermostat',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <ClimateEntityFeature: 387>,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-MatterThermostat-513-0',
'unit_of_measurement': None,
})
# ---
# name: test_climates[mock_thermostat][climate.mock_thermostat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 18.0,
'friendly_name': 'Mock Thermostat',
'hvac_action': <HVACAction.HEATING: 'heating'>,
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
]),
'max_temp': 32.0,
'min_temp': 7.0,
'supported_features': <ClimateEntityFeature: 387>,
'target_temp_high': 26.0,
'target_temp_low': 20.0,
'temperature': None,
}),
'context': <ANY>,
'entity_id': 'climate.mock_thermostat',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'heat_cool',
})
# ---
# name: test_climates[room_airconditioner][climate.room_airconditioner-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -1105,6 +1105,75 @@
'state': 'Vacuum & Mop',
})
# ---
# name: test_selects[eufy_vacuum_omni_e28][select.2bavs_ab6031x_44pe_clean_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'Auto Mop & Vacuum',
'Auto Vacuum',
'Auto Deep Mop & Vacuum',
'Quick Mop & Vacuum',
'Quick Vacuum',
'Low Noise Mop & Vacuum',
'Low Noise Vacuum',
'Eufy Customize Mode',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.2bavs_ab6031x_44pe_clean_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Clean mode',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'clean_mode',
'unique_id': '00000000000004D2-0000000000000028-MatterNodeDevice-1-MatterRvcCleanMode-85-1',
'unit_of_measurement': None,
})
# ---
# name: test_selects[eufy_vacuum_omni_e28][select.2bavs_ab6031x_44pe_clean_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '2BAVS-AB6031X-44PE Clean mode',
'options': list([
'Auto Mop & Vacuum',
'Auto Vacuum',
'Auto Deep Mop & Vacuum',
'Quick Mop & Vacuum',
'Quick Vacuum',
'Low Noise Mop & Vacuum',
'Low Noise Vacuum',
'Eufy Customize Mode',
]),
}),
'context': <ANY>,
'entity_id': 'select.2bavs_ab6031x_44pe_clean_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'Auto Mop & Vacuum',
})
# ---
# name: test_selects[eve_energy_20ecn4101][select.eve_energy_20ecn4101_power_on_behavior_on_startup_bottom-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -2570,6 +2639,63 @@
'state': 'silent',
})
# ---
# name: test_selects[mock_thermostat][select.mock_thermostat_temperature_display_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'Celsius',
'Fahrenheit',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.mock_thermostat_temperature_display_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Temperature display mode',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature_display_mode',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-TrvTemperatureDisplayMode-516-0',
'unit_of_measurement': None,
})
# ---
# name: test_selects[mock_thermostat][select.mock_thermostat_temperature_display_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Thermostat Temperature display mode',
'options': list([
'Celsius',
'Fahrenheit',
]),
}),
'context': <ANY>,
'entity_id': 'select.mock_thermostat_temperature_display_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'Celsius',
})
# ---
# name: test_selects[mounted_dimmable_load_control_fixture][select.mock_mounted_dimmable_load_control_power_on_behavior_on_startup-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -3786,6 +3786,279 @@
'state': '0.0',
})
# ---
# name: test_sensors[eufy_vacuum_omni_e28][sensor.2bavs_ab6031x_44pe_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.2bavs_ab6031x_44pe_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'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-0000000000000028-MatterNodeDevice-1-PowerSource-47-12',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[eufy_vacuum_omni_e28][sensor.2bavs_ab6031x_44pe_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': '2BAVS-AB6031X-44PE Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.2bavs_ab6031x_44pe_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_sensors[eufy_vacuum_omni_e28][sensor.2bavs_ab6031x_44pe_battery_charge_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'not_charging',
'charging',
'full_charge',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.2bavs_ab6031x_44pe_battery_charge_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Battery charge state',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'battery_charge_state',
'unique_id': '00000000000004D2-0000000000000028-MatterNodeDevice-1-PowerSourceBatChargeState-47-26',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[eufy_vacuum_omni_e28][sensor.2bavs_ab6031x_44pe_battery_charge_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': '2BAVS-AB6031X-44PE Battery charge state',
'options': list([
'not_charging',
'charging',
'full_charge',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.2bavs_ab6031x_44pe_battery_charge_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'full_charge',
})
# ---
# name: test_sensors[eufy_vacuum_omni_e28][sensor.2bavs_ab6031x_44pe_operational_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'no_error',
'unable_to_start_or_resume',
'unable_to_complete_operation',
'command_invalid_in_state',
'failed_to_find_charging_dock',
'stuck',
'dust_bin_missing',
'dust_bin_full',
'water_tank_empty',
'water_tank_missing',
'water_tank_lid_open',
'mop_cleaning_pad_missing',
'low_battery',
'cannot_reach_target_area',
'dirty_water_tank_full',
'dirty_water_tank_missing',
'wheels_jammed',
'brush_jammed',
'navigation_sensor_obscured',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.2bavs_ab6031x_44pe_operational_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Operational error',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'operational_error',
'unique_id': '00000000000004D2-0000000000000028-MatterNodeDevice-1-RvcOperationalStateOperationalError-97-5',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[eufy_vacuum_omni_e28][sensor.2bavs_ab6031x_44pe_operational_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': '2BAVS-AB6031X-44PE Operational error',
'options': list([
'no_error',
'unable_to_start_or_resume',
'unable_to_complete_operation',
'command_invalid_in_state',
'failed_to_find_charging_dock',
'stuck',
'dust_bin_missing',
'dust_bin_full',
'water_tank_empty',
'water_tank_missing',
'water_tank_lid_open',
'mop_cleaning_pad_missing',
'low_battery',
'cannot_reach_target_area',
'dirty_water_tank_full',
'dirty_water_tank_missing',
'wheels_jammed',
'brush_jammed',
'navigation_sensor_obscured',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.2bavs_ab6031x_44pe_operational_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'no_error',
})
# ---
# name: test_sensors[eufy_vacuum_omni_e28][sensor.2bavs_ab6031x_44pe_operational_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'stopped',
'running',
'paused',
'error',
'seeking_charger',
'charging',
'docked',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.2bavs_ab6031x_44pe_operational_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Operational state',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'operational_state',
'unique_id': '00000000000004D2-0000000000000028-MatterNodeDevice-1-RvcOperationalState-97-4',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[eufy_vacuum_omni_e28][sensor.2bavs_ab6031x_44pe_operational_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': '2BAVS-AB6031X-44PE Operational state',
'options': list([
'stopped',
'running',
'paused',
'error',
'seeking_charger',
'charging',
'docked',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.2bavs_ab6031x_44pe_operational_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'stopped',
})
# ---
# name: test_sensors[eve_contact_sensor][sensor.eve_door_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -7259,6 +7532,167 @@
'state': 'stopped',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_heating_demand-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.mock_thermostat_heating_demand',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Heating demand',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'pi_heating_demand',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatPIHeatingDemand-513-8',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_heating_demand-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Thermostat Heating demand',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_thermostat_heating_demand',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '25',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_outdoor_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mock_thermostat_outdoor_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Outdoor temperature',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'outdoor_temperature',
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatOutdoorTemperature-513-1',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_outdoor_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Mock Thermostat Outdoor temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.mock_thermostat_outdoor_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '5.0',
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mock_thermostat_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatLocalTemperature-513-0',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Mock Thermostat Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.mock_thermostat_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '18.0',
})
# ---
# name: test_sensors[multi_endpoint_light][sensor.inovelli_current_switch_position_config-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -48,6 +48,55 @@
'state': 'docked',
})
# ---
# name: test_vacuum[eufy_vacuum_omni_e28][vacuum.2bavs_ab6031x_44pe-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'vacuum',
'entity_category': None,
'entity_id': 'vacuum.2bavs_ab6031x_44pe',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <VacuumEntityFeature: 12828>,
'translation_key': None,
'unique_id': '00000000000004D2-0000000000000028-MatterNodeDevice-1-MatterVacuumCleaner-84-1',
'unit_of_measurement': None,
})
# ---
# name: test_vacuum[eufy_vacuum_omni_e28][vacuum.2bavs_ab6031x_44pe-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '2BAVS-AB6031X-44PE',
'supported_features': <VacuumEntityFeature: 12828>,
}),
'context': <ANY>,
'entity_id': 'vacuum.2bavs_ab6031x_44pe',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'idle',
})
# ---
# name: test_vacuum[switchbot_k11_plus][vacuum.k11-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -435,3 +435,160 @@ async def test_shutter_problem(
state = hass.states.get("binary_sensor.eve_shutter_switch_20eci1701_problem")
assert state
assert state.state == "on"
@pytest.mark.parametrize("node_fixture", ["mock_thermostat"])
async def test_thermostat_remote_sensing(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test thermostat remote sensing binary sensors."""
remote_sensing_attribute = clusters.Thermostat.Attributes.RemoteSensing
# Test initial state (RemoteSensing = 0, all bits off)
state = hass.states.get(
"binary_sensor.mock_thermostat_local_temperature_remote_sensing"
)
assert state
assert state.state == "off"
state = hass.states.get(
"binary_sensor.mock_thermostat_outdoor_temperature_remote_sensing"
)
assert state
assert state.state == "off"
state = hass.states.get("binary_sensor.mock_thermostat_occupancy_remote_sensing")
assert state
assert state.state == "off"
# Set LocalTemperature bit (bit 0)
set_node_attribute(
matter_node,
1,
remote_sensing_attribute.cluster_id,
remote_sensing_attribute.attribute_id,
1,
)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(
"binary_sensor.mock_thermostat_local_temperature_remote_sensing"
)
assert state
assert state.state == "on"
state = hass.states.get(
"binary_sensor.mock_thermostat_outdoor_temperature_remote_sensing"
)
assert state
assert state.state == "off"
state = hass.states.get("binary_sensor.mock_thermostat_occupancy_remote_sensing")
assert state
assert state.state == "off"
# Set OutdoorTemperature bit (bit 1)
set_node_attribute(
matter_node,
1,
remote_sensing_attribute.cluster_id,
remote_sensing_attribute.attribute_id,
2,
)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(
"binary_sensor.mock_thermostat_local_temperature_remote_sensing"
)
assert state
assert state.state == "off"
state = hass.states.get(
"binary_sensor.mock_thermostat_outdoor_temperature_remote_sensing"
)
assert state
assert state.state == "on"
state = hass.states.get("binary_sensor.mock_thermostat_occupancy_remote_sensing")
assert state
assert state.state == "off"
# Set Occupancy bit (bit 2)
set_node_attribute(
matter_node,
1,
remote_sensing_attribute.cluster_id,
remote_sensing_attribute.attribute_id,
4,
)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(
"binary_sensor.mock_thermostat_local_temperature_remote_sensing"
)
assert state
assert state.state == "off"
state = hass.states.get(
"binary_sensor.mock_thermostat_outdoor_temperature_remote_sensing"
)
assert state
assert state.state == "off"
state = hass.states.get("binary_sensor.mock_thermostat_occupancy_remote_sensing")
assert state
assert state.state == "on"
# Set multiple bits (bits 0 and 2 = value 5)
set_node_attribute(
matter_node,
1,
remote_sensing_attribute.cluster_id,
remote_sensing_attribute.attribute_id,
5,
)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(
"binary_sensor.mock_thermostat_local_temperature_remote_sensing"
)
assert state
assert state.state == "on"
state = hass.states.get(
"binary_sensor.mock_thermostat_outdoor_temperature_remote_sensing"
)
assert state
assert state.state == "off"
state = hass.states.get("binary_sensor.mock_thermostat_occupancy_remote_sensing")
assert state
assert state.state == "on"
# Set all bits (value 7)
set_node_attribute(
matter_node,
1,
remote_sensing_attribute.cluster_id,
remote_sensing_attribute.attribute_id,
7,
)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(
"binary_sensor.mock_thermostat_local_temperature_remote_sensing"
)
assert state
assert state.state == "on"
state = hass.states.get(
"binary_sensor.mock_thermostat_outdoor_temperature_remote_sensing"
)
assert state
assert state.state == "on"
state = hass.states.get("binary_sensor.mock_thermostat_occupancy_remote_sensing")
assert state
assert state.state == "on"

View File

@@ -1584,6 +1584,7 @@ async def test_discovery_with_object_id(
async def test_discovery_with_default_entity_id_for_previous_deleted_entity(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
entity_registry: er.EntityRegistry,
) -> None:
"""Test discovering an MQTT entity with default_entity_id and unique_id."""
@@ -1598,6 +1599,7 @@ async def test_discovery_with_default_entity_id_for_previous_deleted_entity(
)
initial_entity_id = "sensor.hello_id"
new_entity_id = "sensor.updated_hello_id"
later_entity_id = "sensor.later_hello_id"
name = "Hello World 11"
domain = "sensor"
@@ -1626,6 +1628,14 @@ async def test_discovery_with_default_entity_id_for_previous_deleted_entity(
assert state.name == name
assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered
# Assert the entity ID can be changed later
entity_registry.async_update_entity(new_entity_id, new_entity_id=later_entity_id)
await hass.async_block_till_done()
state = hass.states.get(later_entity_id)
assert state is not None
assert state.name == name
async def test_discovery_incl_nodeid(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator

View File

@@ -18,7 +18,7 @@
},
"flow_rate": {
"value": 150,
"unit": "L/s"
"unit": "l/s"
},
"ph_type_dosing": {
"value": "alcalyne",
@@ -75,6 +75,14 @@
"orp_calibration_slope": {
"value": 0.96,
"unit": "mV"
},
"water_meter_total_permanent": {
"value": 12345.67,
"unit": "m3"
},
"water_meter_total_resettable": {
"value": 123.45,
"unit": "m3"
}
},
"binary_sensor": {

View File

@@ -1054,3 +1054,59 @@
'state': '25',
})
# ---
# name: test_all_sensors[sensor.pool_device_totalizer-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.pool_device_totalizer',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.VOLUME: 'volume'>,
'original_icon': None,
'original_name': 'Totalizer',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'water_meter_total_permanent',
'unique_id': 'TEST123456789_water_meter_total_permanent',
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
})
# ---
# name: test_all_sensors[sensor.pool_device_totalizer-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'volume',
'friendly_name': 'Pool Device Totalizer',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.pool_device_totalizer',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '12345.67',
})
# ---

View File

@@ -192,9 +192,7 @@ async def test_block_number_set_value(
{ATTR_ENTITY_ID: "number.test_name_valve_position", ATTR_VALUE: 30},
blocking=True,
)
mock_block_device.http_request.assert_called_once_with(
"get", "thermostat/0", {"pos": 30.0}
)
mock_block_device.set_thermostat_state.assert_called_once_with(0, pos=30.0)
async def test_block_set_value_connection_error(
@@ -208,7 +206,7 @@ async def test_block_set_value_connection_error(
)
monkeypatch.setattr(
mock_block_device,
"http_request",
"set_thermostat_state",
AsyncMock(side_effect=DeviceConnectionError),
)
await init_integration(hass, 1, sleep_period=3600)
@@ -240,7 +238,7 @@ async def test_block_set_value_auth_error(
)
monkeypatch.setattr(
mock_block_device,
"http_request",
"set_thermostat_state",
AsyncMock(side_effect=InvalidAuthError),
)
entry = await init_integration(hass, 1, sleep_period=3600)

Some files were not shown because too many files have changed in this diff Show More