mirror of
https://github.com/home-assistant/core.git
synced 2025-11-20 08:20:12 +00:00
Compare commits
26 Commits
setpoint_c
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
991b8d2040 | ||
|
|
43fadbf6b4 | ||
|
|
ca79d37135 | ||
|
|
df8ef15535 | ||
|
|
249c1530d0 | ||
|
|
081b769abc | ||
|
|
b8b101d747 | ||
|
|
a19be192e0 | ||
|
|
92da82a200 | ||
|
|
820ba1dfba | ||
|
|
63c8962f09 | ||
|
|
c1a6996549 | ||
|
|
05253841af | ||
|
|
f2ef0503a0 | ||
|
|
938da38fc3 | ||
|
|
9311a87bf5 | ||
|
|
b45294ded3 | ||
|
|
82d3190016 | ||
|
|
d8cbcc1977 | ||
|
|
4b69543515 | ||
|
|
97ef4a35b9 | ||
|
|
f782c78650 | ||
|
|
139ed34c74 | ||
|
|
7f14d013ac | ||
|
|
963e27dda4 | ||
|
|
b8e3d57fea |
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1736,6 +1736,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||
/homeassistant/components/vicare/ @CFenner
|
||||
/tests/components/vicare/ @CFenner
|
||||
/homeassistant/components/victron_ble/ @rajlaud
|
||||
/tests/components/victron_ble/ @rajlaud
|
||||
/homeassistant/components/victron_remote_monitoring/ @AndyTempel
|
||||
/tests/components/victron_remote_monitoring/ @AndyTempel
|
||||
/homeassistant/components/vilfo/ @ManneW
|
||||
|
||||
5
homeassistant/brands/victron.json
Normal file
5
homeassistant/brands/victron.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "victron",
|
||||
"name": "Victron",
|
||||
"integrations": ["victron_ble", "victron_remote_monitoring"]
|
||||
}
|
||||
@@ -6,9 +6,8 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import Event, HassJob, HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -20,7 +19,7 @@ from .analytics import (
|
||||
EntityAnalyticsModifications,
|
||||
async_devices_payload,
|
||||
)
|
||||
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
||||
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, PREFERENCE_SCHEMA
|
||||
from .http import AnalyticsDevicesView
|
||||
|
||||
__all__ = [
|
||||
@@ -43,28 +42,9 @@ async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
|
||||
# Load stored data
|
||||
await analytics.load()
|
||||
|
||||
@callback
|
||||
def start_schedule(_event: Event) -> None:
|
||||
async def start_schedule(_event: Event) -> None:
|
||||
"""Start the send schedule after the started event."""
|
||||
# Wait 15 min after started
|
||||
async_call_later(
|
||||
hass,
|
||||
900,
|
||||
HassJob(
|
||||
analytics.send_analytics,
|
||||
name="analytics schedule",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
# Send every day
|
||||
async_track_time_interval(
|
||||
hass,
|
||||
analytics.send_analytics,
|
||||
INTERVAL,
|
||||
name="analytics daily",
|
||||
cancel_on_shutdown=True,
|
||||
)
|
||||
await analytics.async_schedule()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
|
||||
|
||||
@@ -111,7 +91,7 @@ async def websocket_analytics_preferences(
|
||||
analytics = hass.data[DATA_COMPONENT]
|
||||
|
||||
await analytics.save_preferences(preferences)
|
||||
await analytics.send_analytics()
|
||||
await analytics.async_schedule()
|
||||
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
|
||||
@@ -7,6 +7,8 @@ from asyncio import timeout
|
||||
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
||||
from dataclasses import asdict as dataclass_asdict, dataclass, field
|
||||
from datetime import datetime
|
||||
import random
|
||||
import time
|
||||
from typing import Any, Protocol
|
||||
import uuid
|
||||
|
||||
@@ -31,10 +33,18 @@ from homeassistant.const import (
|
||||
BASE_PLATFORMS,
|
||||
__version__ as HA_VERSION,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
HassJob,
|
||||
HomeAssistant,
|
||||
ReleaseChannel,
|
||||
callback,
|
||||
get_release_channel,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
@@ -51,6 +61,7 @@ from homeassistant.setup import async_get_loaded_integrations
|
||||
from .const import (
|
||||
ANALYTICS_ENDPOINT_URL,
|
||||
ANALYTICS_ENDPOINT_URL_DEV,
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
|
||||
ATTR_ADDON_COUNT,
|
||||
ATTR_ADDONS,
|
||||
ATTR_ARCH,
|
||||
@@ -71,6 +82,7 @@ from .const import (
|
||||
ATTR_PROTECTED,
|
||||
ATTR_RECORDER,
|
||||
ATTR_SLUG,
|
||||
ATTR_SNAPSHOTS,
|
||||
ATTR_STATE_COUNT,
|
||||
ATTR_STATISTICS,
|
||||
ATTR_SUPERVISOR,
|
||||
@@ -80,8 +92,10 @@ from .const import (
|
||||
ATTR_UUID,
|
||||
ATTR_VERSION,
|
||||
DOMAIN,
|
||||
INTERVAL,
|
||||
LOGGER,
|
||||
PREFERENCE_SCHEMA,
|
||||
SNAPSHOT_VERSION,
|
||||
STORAGE_KEY,
|
||||
STORAGE_VERSION,
|
||||
)
|
||||
@@ -194,13 +208,18 @@ def gen_uuid() -> str:
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
RELEASE_CHANNEL = get_release_channel()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalyticsData:
|
||||
"""Analytics data."""
|
||||
|
||||
onboarded: bool
|
||||
preferences: dict[str, bool]
|
||||
uuid: str | None
|
||||
uuid: str | None = None
|
||||
submission_identifier: str | None = None
|
||||
snapshot_submission_time: float | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> AnalyticsData:
|
||||
@@ -209,6 +228,8 @@ class AnalyticsData:
|
||||
data["onboarded"],
|
||||
data["preferences"],
|
||||
data["uuid"],
|
||||
data.get("submission_identifier"),
|
||||
data.get("snapshot_submission_time"),
|
||||
)
|
||||
|
||||
|
||||
@@ -219,8 +240,10 @@ class Analytics:
|
||||
"""Initialize the Analytics class."""
|
||||
self.hass: HomeAssistant = hass
|
||||
self.session = async_get_clientsession(hass)
|
||||
self._data = AnalyticsData(False, {}, None)
|
||||
self._data = AnalyticsData(False, {})
|
||||
self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
self._basic_scheduled: CALLBACK_TYPE | None = None
|
||||
self._snapshot_scheduled: CALLBACK_TYPE | None = None
|
||||
|
||||
@property
|
||||
def preferences(self) -> dict:
|
||||
@@ -228,6 +251,7 @@ class Analytics:
|
||||
preferences = self._data.preferences
|
||||
return {
|
||||
ATTR_BASE: preferences.get(ATTR_BASE, False),
|
||||
ATTR_SNAPSHOTS: preferences.get(ATTR_SNAPSHOTS, False),
|
||||
ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False),
|
||||
ATTR_USAGE: preferences.get(ATTR_USAGE, False),
|
||||
ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False),
|
||||
@@ -244,9 +268,9 @@ class Analytics:
|
||||
return self._data.uuid
|
||||
|
||||
@property
|
||||
def endpoint(self) -> str:
|
||||
def endpoint_basic(self) -> str:
|
||||
"""Return the endpoint that will receive the payload."""
|
||||
if HA_VERSION.endswith("0.dev0"):
|
||||
if RELEASE_CHANNEL is ReleaseChannel.DEV:
|
||||
# dev installations will contact the dev analytics environment
|
||||
return ANALYTICS_ENDPOINT_URL_DEV
|
||||
return ANALYTICS_ENDPOINT_URL
|
||||
@@ -277,13 +301,17 @@ class Analytics:
|
||||
):
|
||||
self._data.preferences[ATTR_DIAGNOSTICS] = False
|
||||
|
||||
async def _save(self) -> None:
|
||||
"""Save data."""
|
||||
await self._store.async_save(dataclass_asdict(self._data))
|
||||
|
||||
async def save_preferences(self, preferences: dict) -> None:
|
||||
"""Save preferences."""
|
||||
preferences = PREFERENCE_SCHEMA(preferences)
|
||||
self._data.preferences.update(preferences)
|
||||
self._data.onboarded = True
|
||||
|
||||
await self._store.async_save(dataclass_asdict(self._data))
|
||||
await self._save()
|
||||
|
||||
if self.supervisor:
|
||||
await hassio.async_update_diagnostics(
|
||||
@@ -292,17 +320,16 @@ class Analytics:
|
||||
|
||||
async def send_analytics(self, _: datetime | None = None) -> None:
|
||||
"""Send analytics."""
|
||||
if not self.onboarded or not self.preferences.get(ATTR_BASE, False):
|
||||
return
|
||||
|
||||
hass = self.hass
|
||||
supervisor_info = None
|
||||
operating_system_info: dict[str, Any] = {}
|
||||
|
||||
if not self.onboarded or not self.preferences.get(ATTR_BASE, False):
|
||||
LOGGER.debug("Nothing to submit")
|
||||
return
|
||||
|
||||
if self._data.uuid is None:
|
||||
self._data.uuid = gen_uuid()
|
||||
await self._store.async_save(dataclass_asdict(self._data))
|
||||
await self._save()
|
||||
|
||||
if self.supervisor:
|
||||
supervisor_info = hassio.get_supervisor_info(hass)
|
||||
@@ -436,7 +463,7 @@ class Analytics:
|
||||
|
||||
try:
|
||||
async with timeout(30):
|
||||
response = await self.session.post(self.endpoint, json=payload)
|
||||
response = await self.session.post(self.endpoint_basic, json=payload)
|
||||
if response.status == 200:
|
||||
LOGGER.info(
|
||||
(
|
||||
@@ -449,7 +476,7 @@ class Analytics:
|
||||
LOGGER.warning(
|
||||
"Sending analytics failed with statuscode %s from %s",
|
||||
response.status,
|
||||
self.endpoint,
|
||||
self.endpoint_basic,
|
||||
)
|
||||
except TimeoutError:
|
||||
LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL)
|
||||
@@ -489,6 +516,182 @@ class Analytics:
|
||||
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
|
||||
)
|
||||
|
||||
async def send_snapshot(self, _: datetime | None = None) -> None:
|
||||
"""Send a snapshot."""
|
||||
if not self.onboarded or not self.preferences.get(ATTR_SNAPSHOTS, False):
|
||||
return
|
||||
|
||||
payload = await _async_snapshot_payload(self.hass)
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": f"home-assistant/{HA_VERSION}",
|
||||
}
|
||||
if self._data.submission_identifier is not None:
|
||||
headers["X-Device-Database-Submission-Identifier"] = (
|
||||
self._data.submission_identifier
|
||||
)
|
||||
|
||||
try:
|
||||
async with timeout(30):
|
||||
response = await self.session.post(
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL, json=payload, headers=headers
|
||||
)
|
||||
|
||||
if response.status == 200: # OK
|
||||
response_data = await response.json()
|
||||
new_identifier = response_data.get("submission_identifier")
|
||||
|
||||
if (
|
||||
new_identifier is not None
|
||||
and new_identifier != self._data.submission_identifier
|
||||
):
|
||||
self._data.submission_identifier = new_identifier
|
||||
await self._save()
|
||||
|
||||
LOGGER.info(
|
||||
"Submitted snapshot analytics to Home Assistant servers"
|
||||
)
|
||||
|
||||
elif response.status == 400: # Bad Request
|
||||
response_data = await response.json()
|
||||
error_kind = response_data.get("kind", "unknown")
|
||||
error_message = response_data.get("message", "Unknown error")
|
||||
|
||||
if error_kind == "invalid-submission-identifier":
|
||||
# Clear the invalid identifier and retry on next cycle
|
||||
LOGGER.warning(
|
||||
"Invalid submission identifier to %s, clearing: %s",
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
|
||||
error_message,
|
||||
)
|
||||
self._data.submission_identifier = None
|
||||
await self._save()
|
||||
else:
|
||||
LOGGER.warning(
|
||||
"Malformed snapshot analytics submission (%s) to %s: %s",
|
||||
error_kind,
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
|
||||
error_message,
|
||||
)
|
||||
|
||||
elif response.status == 503: # Service Unavailable
|
||||
response_text = await response.text()
|
||||
LOGGER.warning(
|
||||
"Snapshot analytics service %s unavailable: %s",
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
|
||||
response_text,
|
||||
)
|
||||
|
||||
else:
|
||||
LOGGER.warning(
|
||||
"Unexpected status code %s when submitting snapshot analytics to %s",
|
||||
response.status,
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
|
||||
)
|
||||
|
||||
except TimeoutError:
|
||||
LOGGER.error(
|
||||
"Timeout sending snapshot analytics to %s",
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
|
||||
)
|
||||
except aiohttp.ClientError as err:
|
||||
LOGGER.error(
|
||||
"Error sending snapshot analytics to %s: %r",
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
|
||||
err,
|
||||
)
|
||||
|
||||
async def async_schedule(self) -> None:
|
||||
"""Schedule analytics."""
|
||||
if not self.onboarded:
|
||||
LOGGER.debug("Analytics not scheduled")
|
||||
if self._basic_scheduled is not None:
|
||||
self._basic_scheduled()
|
||||
self._basic_scheduled = None
|
||||
if self._snapshot_scheduled:
|
||||
self._snapshot_scheduled()
|
||||
self._snapshot_scheduled = None
|
||||
return
|
||||
|
||||
if not self.preferences.get(ATTR_BASE, False):
|
||||
LOGGER.debug("Basic analytics not scheduled")
|
||||
if self._basic_scheduled is not None:
|
||||
self._basic_scheduled()
|
||||
self._basic_scheduled = None
|
||||
elif self._basic_scheduled is None:
|
||||
# Wait 15 min after started for basic analytics
|
||||
self._basic_scheduled = async_call_later(
|
||||
self.hass,
|
||||
900,
|
||||
HassJob(
|
||||
self._async_schedule_basic,
|
||||
name="basic analytics schedule",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
if not self.preferences.get(ATTR_SNAPSHOTS, False) or RELEASE_CHANNEL not in (
|
||||
ReleaseChannel.DEV,
|
||||
ReleaseChannel.NIGHTLY,
|
||||
):
|
||||
LOGGER.debug("Snapshot analytics not scheduled")
|
||||
if self._snapshot_scheduled:
|
||||
self._snapshot_scheduled()
|
||||
self._snapshot_scheduled = None
|
||||
elif self._snapshot_scheduled is None:
|
||||
snapshot_submission_time = self._data.snapshot_submission_time
|
||||
|
||||
if snapshot_submission_time is None:
|
||||
# Randomize the submission time within the 24 hours
|
||||
snapshot_submission_time = random.uniform(0, 86400)
|
||||
self._data.snapshot_submission_time = snapshot_submission_time
|
||||
await self._save()
|
||||
LOGGER.debug(
|
||||
"Initialized snapshot submission time to %s",
|
||||
snapshot_submission_time,
|
||||
)
|
||||
|
||||
# Calculate delay until next submission
|
||||
current_time = time.time()
|
||||
delay = (snapshot_submission_time - current_time) % 86400
|
||||
|
||||
self._snapshot_scheduled = async_call_later(
|
||||
self.hass,
|
||||
delay,
|
||||
HassJob(
|
||||
self._async_schedule_snapshots,
|
||||
name="snapshot analytics schedule",
|
||||
cancel_on_shutdown=True,
|
||||
),
|
||||
)
|
||||
|
||||
async def _async_schedule_basic(self, _: datetime | None = None) -> None:
|
||||
"""Schedule basic analytics."""
|
||||
await self.send_analytics()
|
||||
|
||||
# Send basic analytics every day
|
||||
self._basic_scheduled = async_track_time_interval(
|
||||
self.hass,
|
||||
self.send_analytics,
|
||||
INTERVAL,
|
||||
name="basic analytics daily",
|
||||
cancel_on_shutdown=True,
|
||||
)
|
||||
|
||||
async def _async_schedule_snapshots(self, _: datetime | None = None) -> None:
|
||||
"""Schedule snapshot analytics."""
|
||||
await self.send_snapshot()
|
||||
|
||||
# Send snapshot analytics every day
|
||||
self._snapshot_scheduled = async_track_time_interval(
|
||||
self.hass,
|
||||
self.send_snapshot,
|
||||
INTERVAL,
|
||||
name="snapshot analytics daily",
|
||||
cancel_on_shutdown=True,
|
||||
)
|
||||
|
||||
|
||||
def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
||||
"""Extract domains from the YAML configuration."""
|
||||
@@ -505,8 +708,8 @@ DEFAULT_DEVICE_ANALYTICS_CONFIG = DeviceAnalyticsModifications()
|
||||
DEFAULT_ENTITY_ANALYTICS_CONFIG = EntityAnalyticsModifications()
|
||||
|
||||
|
||||
async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
|
||||
"""Return detailed information about entities and devices."""
|
||||
async def _async_snapshot_payload(hass: HomeAssistant) -> dict: # noqa: C901
|
||||
"""Return detailed information about entities and devices for a snapshot."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
@@ -711,8 +914,13 @@ async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
|
||||
|
||||
entities_info.append(entity_info)
|
||||
|
||||
return integrations_info
|
||||
|
||||
|
||||
async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
"""Return detailed information about entities and devices for a direct download."""
|
||||
return {
|
||||
"version": "home-assistant:1",
|
||||
"version": f"home-assistant:{SNAPSHOT_VERSION}",
|
||||
"home_assistant": HA_VERSION,
|
||||
"integrations": integrations_info,
|
||||
"integrations": await _async_snapshot_payload(hass),
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import voluptuous as vol
|
||||
|
||||
ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1"
|
||||
ANALYTICS_ENDPOINT_URL_DEV = "https://analytics-api-dev.home-assistant.io/v1"
|
||||
SNAPSHOT_VERSION = "1"
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL = f"https://device-database.eco-dev-aws.openhomefoundation.com/api/v1/snapshot/{SNAPSHOT_VERSION}"
|
||||
DOMAIN = "analytics"
|
||||
INTERVAL = timedelta(days=1)
|
||||
STORAGE_KEY = "core.analytics"
|
||||
@@ -38,6 +40,7 @@ ATTR_PREFERENCES = "preferences"
|
||||
ATTR_PROTECTED = "protected"
|
||||
ATTR_RECORDER = "recorder"
|
||||
ATTR_SLUG = "slug"
|
||||
ATTR_SNAPSHOTS = "snapshots"
|
||||
ATTR_STATE_COUNT = "state_count"
|
||||
ATTR_STATISTICS = "statistics"
|
||||
ATTR_SUPERVISOR = "supervisor"
|
||||
@@ -51,6 +54,7 @@ ATTR_VERSION = "version"
|
||||
PREFERENCE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_BASE): bool,
|
||||
vol.Optional(ATTR_SNAPSHOTS): bool,
|
||||
vol.Optional(ATTR_DIAGNOSTICS): bool,
|
||||
vol.Optional(ATTR_STATISTICS): bool,
|
||||
vol.Optional(ATTR_USAGE): bool,
|
||||
|
||||
@@ -7,3 +7,26 @@ CONNECTION_TIMEOUT: int = 10
|
||||
|
||||
# Field name of last self test retrieved from apcupsd.
|
||||
LAST_S_TEST: Final = "laststest"
|
||||
|
||||
# Mapping of deprecated sensor keys (as reported by apcupsd, lower-cased) to their deprecation
|
||||
# repair issue translation keys.
|
||||
DEPRECATED_SENSORS: Final = {
|
||||
"apc": "apc_deprecated",
|
||||
"end apc": "date_deprecated",
|
||||
"date": "date_deprecated",
|
||||
"apcmodel": "available_via_device_info",
|
||||
"model": "available_via_device_info",
|
||||
"firmware": "available_via_device_info",
|
||||
"version": "available_via_device_info",
|
||||
"upsname": "available_via_device_info",
|
||||
"serialno": "available_via_device_info",
|
||||
}
|
||||
|
||||
AVAILABLE_VIA_DEVICE_ATTR: Final = {
|
||||
"apcmodel": "model",
|
||||
"model": "model",
|
||||
"firmware": "hw_version",
|
||||
"version": "sw_version",
|
||||
"upsname": "name",
|
||||
"serialno": "serial_number",
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
@@ -22,9 +24,11 @@ from homeassistant.const import (
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
import homeassistant.helpers.issue_registry as ir
|
||||
|
||||
from .const import LAST_S_TEST
|
||||
from .const import AVAILABLE_VIA_DEVICE_ATTR, DEPRECATED_SENSORS, DOMAIN, LAST_S_TEST
|
||||
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
||||
from .entity import APCUPSdEntity
|
||||
|
||||
@@ -528,3 +532,62 @@ class APCUPSdSensor(APCUPSdEntity, SensorEntity):
|
||||
self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key])
|
||||
if not self.native_unit_of_measurement:
|
||||
self._attr_native_unit_of_measurement = inferred_unit
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle when entity is added to Home Assistant.
|
||||
|
||||
If this is a deprecated sensor entity, create a repair issue to guide
|
||||
the user to disable it.
|
||||
"""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
reason = DEPRECATED_SENSORS.get(self.entity_description.key)
|
||||
if not reason:
|
||||
return
|
||||
|
||||
automations = automations_with_entity(self.hass, self.entity_id)
|
||||
scripts = scripts_with_entity(self.hass, self.entity_id)
|
||||
if not automations and not scripts:
|
||||
return
|
||||
|
||||
entity_registry = er.async_get(self.hass)
|
||||
items = [
|
||||
f"- [{entry.name or entry.original_name or entity_id}]"
|
||||
f"(/config/{integration}/edit/{entry.unique_id or entity_id.split('.', 1)[-1]})"
|
||||
for integration, entities in (
|
||||
("automation", automations),
|
||||
("script", scripts),
|
||||
)
|
||||
for entity_id in entities
|
||||
if (entry := entity_registry.async_get(entity_id))
|
||||
]
|
||||
placeholders = {
|
||||
"entity_name": str(self.name or self.entity_id),
|
||||
"entity_id": self.entity_id,
|
||||
"items": "\n".join(items),
|
||||
}
|
||||
if via_attr := AVAILABLE_VIA_DEVICE_ATTR.get(self.entity_description.key):
|
||||
placeholders["available_via_device_attr"] = via_attr
|
||||
if device_entry := self.device_entry:
|
||||
placeholders["device_id"] = device_entry.id
|
||||
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"{reason}_{self.entity_id}",
|
||||
breaks_in_ha_version="2026.6.0",
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=reason,
|
||||
translation_placeholders=placeholders,
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Handle when entity will be removed from Home Assistant."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
if issue_key := DEPRECATED_SENSORS.get(self.entity_description.key):
|
||||
ir.async_delete_issue(self.hass, DOMAIN, f"{issue_key}_{self.entity_id}")
|
||||
|
||||
@@ -241,5 +241,19 @@
|
||||
"cannot_connect": {
|
||||
"message": "Cannot connect to APC UPS Daemon."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"apc_deprecated": {
|
||||
"description": "The {entity_name} sensor (`{entity_id}`) is deprecated because it exposes internal details of the APC UPS Daemon response.\n\nIt is still referenced in the following automations or scripts:\n{items}\n\nUpdate those automations or scripts to use supported APC UPS entities instead. Reload the APC UPS Daemon integration afterwards to resolve this issue.",
|
||||
"title": "{entity_name} sensor is deprecated"
|
||||
},
|
||||
"available_via_device_info": {
|
||||
"description": "The {entity_name} sensor (`{entity_id}`) is deprecated because the same value is available from the device registry via `device_attr(\"{device_id}\", \"{available_via_device_attr}\")`.\n\nIt is still referenced in the following automations or scripts:\n{items}\n\nUpdate those automations or scripts to use the `device_attr` helper instead of this sensor. Reload the APC UPS Daemon integration afterwards to resolve this issue.",
|
||||
"title": "{entity_name} sensor is deprecated"
|
||||
},
|
||||
"date_deprecated": {
|
||||
"description": "The {entity_name} sensor (`{entity_id}`) is deprecated because the timestamp is already available from other APC UPS sensors via their last updated time.\n\nIt is still referenced in the following automations or scripts:\n{items}\n\nUpdate those automations or scripts to reference any entity's `last_updated` attribute instead (for example, `states.binary_sensor.apcups_online_status.last_updated`). Reload the APC UPS Daemon integration afterwards to resolve this issue.",
|
||||
"title": "{entity_name} sensor is deprecated"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class BrotherPrinterEntity(CoordinatorEntity[BrotherDataUpdateCoordinator]):
|
||||
connections={(CONNECTION_NETWORK_MAC, coordinator.brother.mac)},
|
||||
serial_number=coordinator.brother.serial,
|
||||
manufacturer="Brother",
|
||||
model=coordinator.brother.model,
|
||||
model_id=coordinator.brother.model,
|
||||
name=coordinator.brother.model,
|
||||
sw_version=coordinator.brother.firmware,
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -345,12 +345,10 @@ class BrotherPrinterSensor(BrotherPrinterEntity, SensorEntity):
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_native_value = description.value(coordinator.data)
|
||||
self._attr_unique_id = f"{coordinator.brother.serial.lower()}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._attr_native_value = self.entity_description.value(self.coordinator.data)
|
||||
self.async_write_ha_state()
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the native value of the sensor."""
|
||||
return self.entity_description.value(self.coordinator.data)
|
||||
|
||||
@@ -18,6 +18,7 @@ def async_setup(hass: HomeAssistant) -> bool:
|
||||
websocket_api.async_register_command(hass, websocket_create_area)
|
||||
websocket_api.async_register_command(hass, websocket_delete_area)
|
||||
websocket_api.async_register_command(hass, websocket_update_area)
|
||||
websocket_api.async_register_command(hass, websocket_reorder_areas)
|
||||
return True
|
||||
|
||||
|
||||
@@ -145,3 +146,27 @@ def websocket_update_area(
|
||||
connection.send_error(msg["id"], "invalid_info", str(err))
|
||||
else:
|
||||
connection.send_result(msg["id"], entry.json_fragment)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "config/area_registry/reorder",
|
||||
vol.Required("area_ids"): [str],
|
||||
}
|
||||
)
|
||||
@websocket_api.require_admin
|
||||
@callback
|
||||
def websocket_reorder_areas(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Handle reorder areas websocket command."""
|
||||
registry = ar.async_get(hass)
|
||||
|
||||
try:
|
||||
registry.async_reorder(msg["area_ids"])
|
||||
except ValueError as err:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
|
||||
else:
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
@@ -18,6 +18,7 @@ def async_setup(hass: HomeAssistant) -> bool:
|
||||
websocket_api.async_register_command(hass, websocket_create_floor)
|
||||
websocket_api.async_register_command(hass, websocket_delete_floor)
|
||||
websocket_api.async_register_command(hass, websocket_update_floor)
|
||||
websocket_api.async_register_command(hass, websocket_reorder_floors)
|
||||
return True
|
||||
|
||||
|
||||
@@ -127,6 +128,28 @@ def websocket_update_floor(
|
||||
connection.send_result(msg["id"], _entry_dict(entry))
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "config/floor_registry/reorder",
|
||||
vol.Required("floor_ids"): [str],
|
||||
}
|
||||
)
|
||||
@websocket_api.require_admin
|
||||
@callback
|
||||
def websocket_reorder_floors(
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Handle reorder floors websocket command."""
|
||||
registry = fr.async_get(hass)
|
||||
|
||||
try:
|
||||
registry.async_reorder(msg["floor_ids"])
|
||||
except ValueError as err:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
|
||||
else:
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@callback
|
||||
def _entry_dict(entry: FloorEntry) -> dict[str, Any]:
|
||||
"""Convert entry to API format."""
|
||||
|
||||
@@ -11,11 +11,14 @@ import voluptuous as vol
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.websocket_api import ActiveConnection
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
DATA_STORAGE: HassKey[dict[str, UserStore]] = HassKey("frontend_storage")
|
||||
DATA_SYSTEM_STORAGE: HassKey[SystemStore] = HassKey("frontend_system_storage")
|
||||
STORAGE_VERSION_USER_DATA = 1
|
||||
STORAGE_VERSION_SYSTEM_DATA = 1
|
||||
|
||||
|
||||
async def async_setup_frontend_storage(hass: HomeAssistant) -> None:
|
||||
@@ -23,6 +26,9 @@ async def async_setup_frontend_storage(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, websocket_set_user_data)
|
||||
websocket_api.async_register_command(hass, websocket_get_user_data)
|
||||
websocket_api.async_register_command(hass, websocket_subscribe_user_data)
|
||||
websocket_api.async_register_command(hass, websocket_set_system_data)
|
||||
websocket_api.async_register_command(hass, websocket_get_system_data)
|
||||
websocket_api.async_register_command(hass, websocket_subscribe_system_data)
|
||||
|
||||
|
||||
async def async_user_store(hass: HomeAssistant, user_id: str) -> UserStore:
|
||||
@@ -83,6 +89,52 @@ class _UserStore(Store[dict[str, Any]]):
|
||||
)
|
||||
|
||||
|
||||
@singleton.singleton(DATA_SYSTEM_STORAGE, async_=True)
|
||||
async def async_system_store(hass: HomeAssistant) -> SystemStore:
|
||||
"""Access the system store."""
|
||||
store = SystemStore(hass)
|
||||
await store.async_load()
|
||||
return store
|
||||
|
||||
|
||||
class SystemStore:
|
||||
"""System store for frontend data."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the system store."""
|
||||
self._store: Store[dict[str, Any]] = Store(
|
||||
hass,
|
||||
STORAGE_VERSION_SYSTEM_DATA,
|
||||
"frontend.system_data",
|
||||
)
|
||||
self.data: dict[str, Any] = {}
|
||||
self.subscriptions: dict[str, list[Callable[[], None]]] = {}
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load the data from the store."""
|
||||
self.data = await self._store.async_load() or {}
|
||||
|
||||
async def async_set_item(self, key: str, value: Any) -> None:
|
||||
"""Set an item and save the store."""
|
||||
self.data[key] = value
|
||||
self._store.async_delay_save(lambda: self.data, 1.0)
|
||||
for cb in self.subscriptions.get(key, []):
|
||||
cb()
|
||||
|
||||
@callback
|
||||
def async_subscribe(
|
||||
self, key: str, on_update_callback: Callable[[], None]
|
||||
) -> Callable[[], None]:
|
||||
"""Subscribe to store updates."""
|
||||
self.subscriptions.setdefault(key, []).append(on_update_callback)
|
||||
|
||||
def unsubscribe() -> None:
|
||||
"""Unsubscribe from the store."""
|
||||
self.subscriptions[key].remove(on_update_callback)
|
||||
|
||||
return unsubscribe
|
||||
|
||||
|
||||
def with_user_store(
|
||||
orig_func: Callable[
|
||||
[HomeAssistant, ActiveConnection, dict[str, Any], UserStore],
|
||||
@@ -107,6 +159,28 @@ def with_user_store(
|
||||
return with_user_store_func
|
||||
|
||||
|
||||
def with_system_store(
|
||||
orig_func: Callable[
|
||||
[HomeAssistant, ActiveConnection, dict[str, Any], SystemStore],
|
||||
Coroutine[Any, Any, None],
|
||||
],
|
||||
) -> Callable[
|
||||
[HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any, None]
|
||||
]:
|
||||
"""Decorate function to provide system store."""
|
||||
|
||||
@wraps(orig_func)
|
||||
async def with_system_store_func(
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Provide system store to function."""
|
||||
store = await async_system_store(hass)
|
||||
|
||||
await orig_func(hass, connection, msg, store)
|
||||
|
||||
return with_system_store_func
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "frontend/set_user_data",
|
||||
@@ -169,3 +243,65 @@ async def websocket_subscribe_user_data(
|
||||
connection.subscriptions[msg["id"]] = store.async_subscribe(key, on_data_update)
|
||||
on_data_update()
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "frontend/set_system_data",
|
||||
vol.Required("key"): str,
|
||||
vol.Required("value"): vol.Any(bool, str, int, float, dict, list, None),
|
||||
}
|
||||
)
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
@with_system_store
|
||||
async def websocket_set_system_data(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
store: SystemStore,
|
||||
) -> None:
|
||||
"""Handle set system data command."""
|
||||
await store.async_set_item(msg["key"], msg["value"])
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{vol.Required("type"): "frontend/get_system_data", vol.Required("key"): str}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@with_system_store
|
||||
async def websocket_get_system_data(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
store: SystemStore,
|
||||
) -> None:
|
||||
"""Handle get system data command."""
|
||||
connection.send_result(msg["id"], {"value": store.data.get(msg["key"])})
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "frontend/subscribe_system_data",
|
||||
vol.Required("key"): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@with_system_store
|
||||
async def websocket_subscribe_system_data(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
store: SystemStore,
|
||||
) -> None:
|
||||
"""Handle subscribe to system data command."""
|
||||
key: str = msg["key"]
|
||||
|
||||
def on_data_update() -> None:
|
||||
"""Handle system data update."""
|
||||
connection.send_event(msg["id"], {"value": store.data.get(key)})
|
||||
|
||||
connection.subscriptions[msg["id"]] = store.async_subscribe(key, on_data_update)
|
||||
on_data_update()
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
"universal-silabs-flasher==0.1.0",
|
||||
"universal-silabs-flasher==0.1.2",
|
||||
"ha-silabs-firmware-client==0.3.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2.7.0"]
|
||||
"requirements": ["aioautomower==2.7.1"]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from asyncio import Task
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
@@ -44,7 +45,7 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
|
||||
_default_update_interval = SCAN_INTERVAL
|
||||
config_entry: LaMarzoccoConfigEntry
|
||||
websocket_terminated = True
|
||||
_websocket_task: Task | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -64,6 +65,13 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
self.device = device
|
||||
self.cloud_client = cloud_client
|
||||
|
||||
@property
|
||||
def websocket_terminated(self) -> bool:
|
||||
"""Return True if the websocket task is terminated or not running."""
|
||||
if self._websocket_task is None:
|
||||
return True
|
||||
return self._websocket_task.done()
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Do the data update."""
|
||||
try:
|
||||
@@ -95,13 +103,14 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
||||
# ensure token stays valid; does nothing if token is still valid
|
||||
await self.cloud_client.async_get_access_token()
|
||||
|
||||
if self.device.websocket.connected:
|
||||
# Only skip websocket reconnection if it's currently connected and the task is still running
|
||||
if self.device.websocket.connected and not self.websocket_terminated:
|
||||
return
|
||||
|
||||
await self.device.get_dashboard()
|
||||
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
|
||||
|
||||
self.config_entry.async_create_background_task(
|
||||
self._websocket_task = self.config_entry.async_create_background_task(
|
||||
hass=self.hass,
|
||||
target=self.connect_websocket(),
|
||||
name="lm_websocket_task",
|
||||
@@ -120,7 +129,6 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
||||
|
||||
_LOGGER.debug("Init WebSocket in background task")
|
||||
|
||||
self.websocket_terminated = False
|
||||
self.async_update_listeners()
|
||||
|
||||
await self.device.connect_dashboard_websocket(
|
||||
@@ -129,7 +137,6 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
||||
disconnect_callback=self.async_update_listeners,
|
||||
)
|
||||
|
||||
self.websocket_terminated = True
|
||||
self.async_update_listeners()
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
import pypck
|
||||
from pypck.connection import (
|
||||
@@ -48,7 +49,6 @@ from .const import (
|
||||
)
|
||||
from .helpers import (
|
||||
AddressType,
|
||||
InputType,
|
||||
LcnConfigEntry,
|
||||
LcnRuntimeData,
|
||||
async_update_config_entry,
|
||||
@@ -285,7 +285,7 @@ def _async_fire_access_control_event(
|
||||
hass: HomeAssistant,
|
||||
device: dr.DeviceEntry | None,
|
||||
address: AddressType,
|
||||
inp: InputType,
|
||||
inp: pypck.inputs.ModStatusAccessControl,
|
||||
) -> None:
|
||||
"""Fire access control event (transponder, transmitter, fingerprint, codelock)."""
|
||||
event_data = {
|
||||
@@ -299,7 +299,11 @@ def _async_fire_access_control_event(
|
||||
|
||||
if inp.periphery == pypck.lcn_defs.AccessControlPeriphery.TRANSMITTER:
|
||||
event_data.update(
|
||||
{"level": inp.level, "key": inp.key, "action": inp.action.value}
|
||||
{
|
||||
"level": inp.level,
|
||||
"key": inp.key,
|
||||
"action": cast(pypck.lcn_defs.KeyAction, inp.action).value,
|
||||
}
|
||||
)
|
||||
|
||||
event_name = f"lcn_{inp.periphery.value.lower()}"
|
||||
@@ -310,7 +314,7 @@ def _async_fire_send_keys_event(
|
||||
hass: HomeAssistant,
|
||||
device: dr.DeviceEntry | None,
|
||||
address: AddressType,
|
||||
inp: InputType,
|
||||
inp: pypck.inputs.ModSendKeysHost,
|
||||
) -> None:
|
||||
"""Fire send_keys event."""
|
||||
for table, action in enumerate(inp.actions):
|
||||
|
||||
@@ -100,8 +100,6 @@ class LcnClimate(LcnEntity, ClimateEntity):
|
||||
self._max_temp = config[CONF_DOMAIN_DATA][CONF_MAX_TEMP]
|
||||
self._min_temp = config[CONF_DOMAIN_DATA][CONF_MIN_TEMP]
|
||||
|
||||
self._current_temperature = None
|
||||
self._target_temperature = None
|
||||
self._is_on = True
|
||||
|
||||
self._attr_hvac_modes = [HVACMode.HEAT]
|
||||
@@ -121,16 +119,6 @@ class LcnClimate(LcnEntity, ClimateEntity):
|
||||
return UnitOfTemperature.FAHRENHEIT
|
||||
return UnitOfTemperature.CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return hvac operation ie. heat, cool mode.
|
||||
@@ -166,7 +154,7 @@ class LcnClimate(LcnEntity, ClimateEntity):
|
||||
):
|
||||
return
|
||||
self._is_on = False
|
||||
self._target_temperature = None
|
||||
self._attr_target_temperature = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
@@ -178,7 +166,7 @@ class LcnClimate(LcnEntity, ClimateEntity):
|
||||
self.setpoint, temperature, self.unit
|
||||
):
|
||||
return
|
||||
self._target_temperature = temperature
|
||||
self._attr_target_temperature = temperature
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
@@ -198,10 +186,14 @@ class LcnClimate(LcnEntity, ClimateEntity):
|
||||
return
|
||||
|
||||
if input_obj.get_var() == self.variable:
|
||||
self._current_temperature = input_obj.get_value().to_var_unit(self.unit)
|
||||
self._attr_current_temperature = float(
|
||||
input_obj.get_value().to_var_unit(self.unit)
|
||||
)
|
||||
elif input_obj.get_var() == self.setpoint:
|
||||
self._is_on = not input_obj.get_value().is_locked_regulator()
|
||||
if self._is_on:
|
||||
self._target_temperature = input_obj.get_value().to_var_unit(self.unit)
|
||||
self._attr_target_temperature = float(
|
||||
input_obj.get_value().to_var_unit(self.unit)
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -120,7 +120,7 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors={CONF_BASE: error},
|
||||
)
|
||||
|
||||
data: dict = {
|
||||
data: dict[str, Any] = {
|
||||
**user_input,
|
||||
CONF_DEVICES: [],
|
||||
CONF_ENTITIES: [],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Support for LCN covers."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Coroutine, Iterable
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
@@ -81,6 +81,8 @@ class LcnOutputsCover(LcnEntity, CoverEntity):
|
||||
_attr_is_opening = False
|
||||
_attr_assumed_state = True
|
||||
|
||||
reverse_time: pypck.lcn_defs.MotorReverseTime | None
|
||||
|
||||
def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
|
||||
"""Initialize the LCN cover."""
|
||||
super().__init__(config, config_entry)
|
||||
@@ -255,7 +257,15 @@ class LcnRelayCover(LcnEntity, CoverEntity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the state of the entity."""
|
||||
coros = [self.device_connection.request_status_relays(SCAN_INTERVAL.seconds)]
|
||||
coros: list[
|
||||
Coroutine[
|
||||
Any,
|
||||
Any,
|
||||
pypck.inputs.ModStatusRelays
|
||||
| pypck.inputs.ModStatusMotorPositionBS4
|
||||
| None,
|
||||
]
|
||||
] = [self.device_connection.request_status_relays(SCAN_INTERVAL.seconds)]
|
||||
if self.positioning_mode == pypck.lcn_defs.MotorPositioningMode.BS4:
|
||||
coros.append(
|
||||
self.device_connection.request_status_motor_position(
|
||||
@@ -283,7 +293,7 @@ class LcnRelayCover(LcnEntity, CoverEntity):
|
||||
)
|
||||
and input_obj.motor == self.motor.value
|
||||
):
|
||||
self._attr_current_cover_position = input_obj.position
|
||||
self._attr_current_cover_position = int(input_obj.position)
|
||||
if self._attr_current_cover_position in [0, 100]:
|
||||
self._attr_is_opening = False
|
||||
self._attr_is_closing = False
|
||||
|
||||
@@ -35,7 +35,7 @@ class LcnEntity(Entity):
|
||||
self.config = config
|
||||
self.config_entry = config_entry
|
||||
self.address: AddressType = config[CONF_ADDRESS]
|
||||
self._unregister_for_inputs: Callable | None = None
|
||||
self._unregister_for_inputs: Callable[[], None] | None = None
|
||||
self._name: str = config[CONF_NAME]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
|
||||
@@ -61,7 +61,7 @@ type LcnConfigEntry = ConfigEntry[LcnRuntimeData]
|
||||
|
||||
type AddressType = tuple[int, int, bool]
|
||||
|
||||
type InputType = type[pypck.inputs.Input]
|
||||
type InputType = pypck.inputs.Input
|
||||
|
||||
# Regex for address validation
|
||||
PATTERN_ADDRESS = re.compile(
|
||||
@@ -269,10 +269,10 @@ async def async_update_device_config(
|
||||
if device_config[CONF_NAME] != "":
|
||||
return
|
||||
|
||||
device_name = ""
|
||||
device_name: str | None = None
|
||||
if not is_group:
|
||||
device_name = await device_connection.request_name()
|
||||
if is_group or device_name == "":
|
||||
if is_group or device_name is None:
|
||||
module_type = "Group" if is_group else "Module"
|
||||
device_name = (
|
||||
f"{module_type} "
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pypck"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pypck==0.9.3", "lcn-frontend==0.2.7"]
|
||||
"requirements": ["pypck==0.9.5", "lcn-frontend==0.2.7"]
|
||||
}
|
||||
|
||||
@@ -74,4 +74,4 @@ rules:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration is not making any HTTP requests.
|
||||
strict-typing: todo
|
||||
strict-typing: done
|
||||
|
||||
@@ -156,6 +156,8 @@ class LcnVariableSensor(LcnEntity, SensorEntity):
|
||||
class LcnLedLogicSensor(LcnEntity, SensorEntity):
|
||||
"""Representation of a LCN sensor for leds and logicops."""
|
||||
|
||||
source: pypck.lcn_defs.LedPort | pypck.lcn_defs.LogicOpPort
|
||||
|
||||
def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
|
||||
"""Initialize the LCN sensor."""
|
||||
super().__init__(config, config_entry)
|
||||
|
||||
@@ -104,7 +104,9 @@ def get_config_entry(
|
||||
|
||||
@wraps(func)
|
||||
async def get_entry(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Get config_entry."""
|
||||
if not (config_entry := hass.config_entries.async_get_entry(msg["entry_id"])):
|
||||
@@ -124,7 +126,7 @@ def get_config_entry(
|
||||
async def websocket_get_device_configs(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict,
|
||||
msg: dict[str, Any],
|
||||
config_entry: LcnConfigEntry,
|
||||
) -> None:
|
||||
"""Get device configs."""
|
||||
@@ -144,7 +146,7 @@ async def websocket_get_device_configs(
|
||||
async def websocket_get_entity_configs(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict,
|
||||
msg: dict[str, Any],
|
||||
config_entry: LcnConfigEntry,
|
||||
) -> None:
|
||||
"""Get entities configs."""
|
||||
@@ -175,7 +177,7 @@ async def websocket_get_entity_configs(
|
||||
async def websocket_scan_devices(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict,
|
||||
msg: dict[str, Any],
|
||||
config_entry: LcnConfigEntry,
|
||||
) -> None:
|
||||
"""Scan for new devices."""
|
||||
@@ -207,7 +209,7 @@ async def websocket_scan_devices(
|
||||
async def websocket_add_device(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict,
|
||||
msg: dict[str, Any],
|
||||
config_entry: LcnConfigEntry,
|
||||
) -> None:
|
||||
"""Add a device."""
|
||||
@@ -253,7 +255,7 @@ async def websocket_add_device(
|
||||
async def websocket_delete_device(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict,
|
||||
msg: dict[str, Any],
|
||||
config_entry: LcnConfigEntry,
|
||||
) -> None:
|
||||
"""Delete a device."""
|
||||
@@ -315,7 +317,7 @@ async def websocket_delete_device(
|
||||
async def websocket_add_entity(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict,
|
||||
msg: dict[str, Any],
|
||||
config_entry: LcnConfigEntry,
|
||||
) -> None:
|
||||
"""Add an entity."""
|
||||
@@ -381,7 +383,7 @@ async def websocket_add_entity(
|
||||
async def websocket_delete_entity(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict,
|
||||
msg: dict[str, Any],
|
||||
config_entry: LcnConfigEntry,
|
||||
) -> None:
|
||||
"""Delete an entity."""
|
||||
@@ -451,7 +453,7 @@ async def async_create_or_update_device_in_config_entry(
|
||||
|
||||
|
||||
def get_entity_entry(
|
||||
hass: HomeAssistant, entity_config: dict, config_entry: LcnConfigEntry
|
||||
hass: HomeAssistant, entity_config: dict[str, Any], config_entry: LcnConfigEntry
|
||||
) -> er.RegistryEntry | None:
|
||||
"""Get entity RegistryEntry from entity_config."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pylutron_caseta"],
|
||||
"requirements": ["pylutron-caseta==0.25.0"],
|
||||
"requirements": ["pylutron-caseta==0.26.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"properties": {
|
||||
|
||||
@@ -183,13 +183,6 @@ PUMP_CONTROL_MODE_MAP = {
|
||||
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kUnknownEnumValue: None,
|
||||
}
|
||||
|
||||
SETPOINT_CHANGE_SOURCE_MAP = {
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kManual: "manual",
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kSchedule: "schedule",
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kExternal: "external",
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kUnknownEnumValue: None,
|
||||
}
|
||||
|
||||
HUMIDITY_SCALING_FACTOR = 100
|
||||
TEMPERATURE_SCALING_FACTOR = 100
|
||||
|
||||
@@ -1495,47 +1488,4 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.ServiceArea.Attributes.EstimatedEndTime,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="SetpointChangeSource",
|
||||
translation_key="setpoint_change_source",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None,
|
||||
# convert to set first to remove the duplicate unknown value
|
||||
options=[x for x in SETPOINT_CHANGE_SOURCE_MAP.values() if x is not None],
|
||||
device_to_ha=lambda x: SETPOINT_CHANGE_SOURCE_MAP[x],
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.Thermostat.Attributes.SetpointChangeSource,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="SetpointChangeSourceTimestamp",
|
||||
translation_key="setpoint_change_timestamp",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=None,
|
||||
device_to_ha=(lambda x: dt_util.utc_from_timestamp(x) if x > 0 else None),
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
clusters.Thermostat.Attributes.SetpointChangeSourceTimestamp,
|
||||
),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="ThermostatSetpointChangeAmount",
|
||||
translation_key="setpoint_change_amount",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=1,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
device_to_ha=lambda x: x / TEMPERATURE_SCALING_FACTOR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.Thermostat.Attributes.SetpointChangeAmount,),
|
||||
device_type=(device_types.Thermostat,),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -223,9 +223,6 @@
|
||||
"pump_setpoint": {
|
||||
"name": "Setpoint"
|
||||
},
|
||||
"setpoint_change_source_timestamp": {
|
||||
"name": "Last change"
|
||||
},
|
||||
"temperature_offset": {
|
||||
"name": "Temperature offset"
|
||||
},
|
||||
@@ -521,20 +518,6 @@
|
||||
"rms_voltage": {
|
||||
"name": "Effective voltage"
|
||||
},
|
||||
"setpoint_change_amount": {
|
||||
"name": "Last change amount"
|
||||
},
|
||||
"setpoint_change_source": {
|
||||
"name": "Last change source",
|
||||
"state": {
|
||||
"external": "External",
|
||||
"manual": "Manual",
|
||||
"schedule": "Schedule"
|
||||
}
|
||||
},
|
||||
"setpoint_change_timestamp": {
|
||||
"name": "Last change"
|
||||
},
|
||||
"switch_current_position": {
|
||||
"name": "Current switch position"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import (
|
||||
OhmeAdvancedSettingsCoordinator,
|
||||
OhmeChargeSessionCoordinator,
|
||||
OhmeConfigEntry,
|
||||
OhmeDeviceInfoCoordinator,
|
||||
@@ -56,7 +55,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: OhmeConfigEntry) -> bool
|
||||
|
||||
coordinators = (
|
||||
OhmeChargeSessionCoordinator(hass, entry, client),
|
||||
OhmeAdvancedSettingsCoordinator(hass, entry, client),
|
||||
OhmeDeviceInfoCoordinator(hass, entry, client),
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
from ohme import ApiException, OhmeApiClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -23,7 +23,6 @@ class OhmeRuntimeData:
|
||||
"""Dataclass to hold ohme coordinators."""
|
||||
|
||||
charge_session_coordinator: OhmeChargeSessionCoordinator
|
||||
advanced_settings_coordinator: OhmeAdvancedSettingsCoordinator
|
||||
device_info_coordinator: OhmeDeviceInfoCoordinator
|
||||
|
||||
|
||||
@@ -78,31 +77,6 @@ class OhmeChargeSessionCoordinator(OhmeBaseCoordinator):
|
||||
await self.client.async_get_charge_session()
|
||||
|
||||
|
||||
class OhmeAdvancedSettingsCoordinator(OhmeBaseCoordinator):
|
||||
"""Coordinator to pull settings and charger state from the API."""
|
||||
|
||||
coordinator_name = "Advanced Settings"
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: OhmeConfigEntry, client: OhmeApiClient
|
||||
) -> None:
|
||||
"""Initialise coordinator."""
|
||||
super().__init__(hass, config_entry, client)
|
||||
|
||||
@callback
|
||||
def _dummy_listener() -> None:
|
||||
pass
|
||||
|
||||
# This coordinator is used by the API library to determine whether the
|
||||
# charger is online and available. It is therefore required even if no
|
||||
# entities are using it.
|
||||
self.async_add_listener(_dummy_listener)
|
||||
|
||||
async def _internal_update_data(self) -> None:
|
||||
"""Fetch data from API endpoint."""
|
||||
await self.client.async_get_advanced_settings()
|
||||
|
||||
|
||||
class OhmeDeviceInfoCoordinator(OhmeBaseCoordinator):
|
||||
"""Coordinator to pull device info and charger settings from the API."""
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["ohme==1.5.2"]
|
||||
"requirements": ["ohme==1.6.0"]
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class OhmeSensorDescription(OhmeEntityDescription, SensorEntityDescription):
|
||||
value_fn: Callable[[OhmeApiClient], str | int | float | None]
|
||||
|
||||
|
||||
SENSOR_CHARGE_SESSION = [
|
||||
SENSORS = [
|
||||
OhmeSensorDescription(
|
||||
key="status",
|
||||
translation_key="status",
|
||||
@@ -91,18 +91,6 @@ SENSOR_CHARGE_SESSION = [
|
||||
),
|
||||
]
|
||||
|
||||
SENSOR_ADVANCED_SETTINGS = [
|
||||
OhmeSensorDescription(
|
||||
key="ct_current",
|
||||
translation_key="ct_current",
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
value_fn=lambda client: client.power.ct_amps,
|
||||
is_supported_fn=lambda client: client.ct_connected,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -110,16 +98,11 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensors."""
|
||||
coordinators = config_entry.runtime_data
|
||||
coordinator_map = [
|
||||
(SENSOR_CHARGE_SESSION, coordinators.charge_session_coordinator),
|
||||
(SENSOR_ADVANCED_SETTINGS, coordinators.advanced_settings_coordinator),
|
||||
]
|
||||
coordinator = config_entry.runtime_data.charge_session_coordinator
|
||||
|
||||
async_add_entities(
|
||||
OhmeSensor(coordinator, description)
|
||||
for entities, coordinator in coordinator_map
|
||||
for description in entities
|
||||
for description in SENSORS
|
||||
if description.is_supported_fn(coordinator.client)
|
||||
)
|
||||
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.0.16"]
|
||||
"requirements": ["onedrive-personal-sdk==0.0.17"]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioshelly"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioshelly==13.17.0"],
|
||||
"requirements": ["aioshelly==13.19.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "shelly*",
|
||||
|
||||
@@ -18,7 +18,6 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN, MANUFACTURER
|
||||
from .types import DaliCenterConfigEntry, DaliCenterData
|
||||
@@ -47,12 +46,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaliCenterConfigEntry) -
|
||||
"You can try to delete the gateway and add it again"
|
||||
) from exc
|
||||
|
||||
def on_online_status(dev_id: str, available: bool) -> None:
|
||||
signal = f"{DOMAIN}_update_available_{dev_id}"
|
||||
hass.add_job(async_dispatcher_send, hass, signal, available)
|
||||
|
||||
gateway.on_online_status = on_online_status
|
||||
|
||||
try:
|
||||
devices = await gateway.discover_devices()
|
||||
except DaliGatewayError as exc:
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from PySrDaliGateway import Device
|
||||
from PySrDaliGateway import CallbackEventType, Device
|
||||
from PySrDaliGateway.helper import is_light_device
|
||||
from PySrDaliGateway.types import LightStatus
|
||||
|
||||
@@ -19,10 +19,6 @@ from homeassistant.components.light import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
@@ -40,15 +36,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Sunricher DALI light entities from config entry."""
|
||||
runtime_data = entry.runtime_data
|
||||
gateway = runtime_data.gateway
|
||||
devices = runtime_data.devices
|
||||
|
||||
def _on_light_status(dev_id: str, status: LightStatus) -> None:
|
||||
signal = f"{DOMAIN}_update_{dev_id}"
|
||||
hass.add_job(async_dispatcher_send, hass, signal, status)
|
||||
|
||||
gateway.on_light_status = _on_light_status
|
||||
|
||||
async_add_entities(
|
||||
DaliCenterLight(device)
|
||||
for device in devices
|
||||
@@ -123,14 +112,16 @@ class DaliCenterLight(LightEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity addition to Home Assistant."""
|
||||
|
||||
signal = f"{DOMAIN}_update_{self._attr_unique_id}"
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, signal, self._handle_device_update)
|
||||
self._light.register_listener(
|
||||
CallbackEventType.LIGHT_STATUS, self._handle_device_update
|
||||
)
|
||||
)
|
||||
|
||||
signal = f"{DOMAIN}_update_available_{self._attr_unique_id}"
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, signal, self._handle_availability)
|
||||
self._light.register_listener(
|
||||
CallbackEventType.ONLINE_STATUS, self._handle_availability
|
||||
)
|
||||
)
|
||||
|
||||
# read_status() only queues a request on the gateway and relies on the
|
||||
@@ -187,4 +178,4 @@ class DaliCenterLight(LightEntity):
|
||||
):
|
||||
self._attr_rgbw_color = status["rgbw_color"]
|
||||
|
||||
self.async_write_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sunricher_dali",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["PySrDaliGateway==0.13.1"]
|
||||
"requirements": ["PySrDaliGateway==0.16.2"]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ rules:
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
|
||||
@@ -851,11 +851,16 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
key=DPCode.EXCRETION_TIME_DAY,
|
||||
translation_key="excretion_time_day",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.EXCRETION_TIMES_DAY,
|
||||
translation_key="excretion_times_day",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.STATUS,
|
||||
translation_key="cat_litter_box_status",
|
||||
),
|
||||
),
|
||||
DeviceCategory.MZJ: (
|
||||
TuyaSensorEntityDescription(
|
||||
|
||||
@@ -617,6 +617,17 @@
|
||||
"carbon_monoxide": {
|
||||
"name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]"
|
||||
},
|
||||
"cat_litter_box_status": {
|
||||
"name": "[%key:component::tuya::entity::sensor::status::name%]",
|
||||
"state": {
|
||||
"clean": "Cleaning",
|
||||
"empty": "Emptying",
|
||||
"level": "Leveling",
|
||||
"sleep": "Sleeping",
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"uv": "Sanitizing"
|
||||
}
|
||||
},
|
||||
"cat_weight": {
|
||||
"name": "Cat weight"
|
||||
},
|
||||
@@ -645,7 +656,7 @@
|
||||
"name": "Duster cloth lifetime"
|
||||
},
|
||||
"excretion_time_day": {
|
||||
"name": "Excretion time (day)"
|
||||
"name": "Excretion duration"
|
||||
},
|
||||
"excretion_times_day": {
|
||||
"name": "Excretion times (day)"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiounifi"],
|
||||
"requirements": ["aiounifi==87"],
|
||||
"requirements": ["aiounifi==88"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -104,6 +104,15 @@ def async_client_uptime_value_fn(hub: UnifiHub, client: Client) -> datetime:
|
||||
return dt_util.utc_from_timestamp(float(client.uptime))
|
||||
|
||||
|
||||
@callback
|
||||
def async_wired_client_allowed_fn(hub: UnifiHub, obj_id: str) -> bool:
|
||||
"""Check if client is wired and allowed."""
|
||||
client = hub.api.clients[obj_id]
|
||||
if not client.is_wired or client.wired_rate_mbps <= 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
def async_wlan_client_value_fn(hub: UnifiHub, wlan: Wlan) -> int:
|
||||
"""Calculate the amount of clients connected to a wlan."""
|
||||
@@ -407,6 +416,23 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = (
|
||||
unique_id_fn=lambda hub, obj_id: f"tx-{obj_id}",
|
||||
value_fn=async_client_tx_value_fn,
|
||||
),
|
||||
UnifiSensorEntityDescription[Clients, Client](
|
||||
key="Wired client speed",
|
||||
translation_key="wired_client_link_speed",
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||
entity_registry_enabled_default=False,
|
||||
allowed_fn=async_wired_client_allowed_fn,
|
||||
api_handler_fn=lambda api: api.clients,
|
||||
device_info_fn=async_client_device_info_fn,
|
||||
is_connected_fn=async_client_is_connected_fn,
|
||||
name_fn=lambda _: "Link speed",
|
||||
object_fn=lambda api, obj_id: api.clients[obj_id],
|
||||
unique_id_fn=lambda hub, obj_id: f"wired_speed-{obj_id}",
|
||||
value_fn=lambda hub, client: client.wired_rate_mbps,
|
||||
),
|
||||
UnifiSensorEntityDescription[Ports, Port](
|
||||
key="PoE port power sensor",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
"provisioning": "Provisioning",
|
||||
"upgrading": "Upgrading"
|
||||
}
|
||||
},
|
||||
"wired_client_link_speed": {
|
||||
"name": "Link speed"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
54
homeassistant/components/victron_ble/__init__.py
Normal file
54
homeassistant/components/victron_ble/__init__.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""The Victron Bluetooth Low Energy integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from victron_ble_ha_parser import VictronBluetoothDeviceData
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothScanningMode,
|
||||
async_rediscover_address,
|
||||
)
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Victron BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
key = entry.data[CONF_ACCESS_TOKEN]
|
||||
data = VictronBluetoothDeviceData(key)
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.ACTIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
|
||||
entry.async_on_unload(coordinator.async_start())
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
entry, [Platform.SENSOR]
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
async_rediscover_address(hass, entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
123
homeassistant/components/victron_ble/config_flow.py
Normal file
123
homeassistant/components/victron_ble/config_flow.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Config flow for Victron Bluetooth Low Energy integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from victron_ble_ha_parser import VictronBluetoothDeviceData
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfoBleak,
|
||||
async_discovered_service_info,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS
|
||||
|
||||
from .const import DOMAIN, VICTRON_IDENTIFIER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_ACCESS_TOKEN_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ACCESS_TOKEN): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class VictronBLEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Victron Bluetooth Low Energy."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._discovered_device: str | None = None
|
||||
self._discovered_devices: dict[str, str] = {}
|
||||
self._discovered_devices_info: dict[str, BluetoothServiceInfoBleak] = {}
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the bluetooth discovery step."""
|
||||
_LOGGER.debug("async_step_bluetooth: %s", discovery_info.address)
|
||||
await self.async_set_unique_id(discovery_info.address)
|
||||
self._abort_if_unique_id_configured()
|
||||
device = VictronBluetoothDeviceData()
|
||||
if not device.supported(discovery_info):
|
||||
_LOGGER.debug("device %s not supported", discovery_info.address)
|
||||
return self.async_abort(reason="not_supported")
|
||||
|
||||
self._discovered_device = discovery_info.address
|
||||
self._discovered_devices_info[discovery_info.address] = discovery_info
|
||||
self._discovered_devices[discovery_info.address] = discovery_info.name
|
||||
|
||||
self.context["title_placeholders"] = {"title": discovery_info.name}
|
||||
|
||||
return await self.async_step_access_token()
|
||||
|
||||
async def async_step_access_token(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle advertisement key input."""
|
||||
# should only be called if there are discovered devices
|
||||
assert self._discovered_device is not None
|
||||
discovery_info = self._discovered_devices_info[self._discovered_device]
|
||||
title = discovery_info.name
|
||||
|
||||
if user_input is not None:
|
||||
# see if we can create a device with the access token
|
||||
device = VictronBluetoothDeviceData(user_input[CONF_ACCESS_TOKEN])
|
||||
if device.validate_advertisement_key(
|
||||
discovery_info.manufacturer_data[VICTRON_IDENTIFIER]
|
||||
):
|
||||
return self.async_create_entry(
|
||||
title=title,
|
||||
data=user_input,
|
||||
)
|
||||
return self.async_abort(reason="invalid_access_token")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="access_token",
|
||||
data_schema=STEP_ACCESS_TOKEN_DATA_SCHEMA,
|
||||
description_placeholders={"title": title},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle select a device to set up."""
|
||||
if user_input is not None:
|
||||
address = user_input[CONF_ADDRESS]
|
||||
await self.async_set_unique_id(address, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
self._discovered_device = address
|
||||
title = self._discovered_devices_info[address].name
|
||||
return self.async_show_form(
|
||||
step_id="access_token",
|
||||
data_schema=STEP_ACCESS_TOKEN_DATA_SCHEMA,
|
||||
description_placeholders={"title": title},
|
||||
)
|
||||
|
||||
current_addresses = self._async_current_ids()
|
||||
for discovery_info in async_discovered_service_info(self.hass, False):
|
||||
address = discovery_info.address
|
||||
if address in current_addresses or address in self._discovered_devices:
|
||||
continue
|
||||
device = VictronBluetoothDeviceData()
|
||||
if device.supported(discovery_info):
|
||||
self._discovered_devices_info[address] = discovery_info
|
||||
self._discovered_devices[address] = discovery_info.name
|
||||
|
||||
if len(self._discovered_devices) < 1:
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
_LOGGER.debug("Discovered %s devices", len(self._discovered_devices))
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)}
|
||||
),
|
||||
)
|
||||
4
homeassistant/components/victron_ble/const.py
Normal file
4
homeassistant/components/victron_ble/const.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Constants for the Victron Bluetooth Low Energy integration."""
|
||||
|
||||
DOMAIN = "victron_ble"
|
||||
VICTRON_IDENTIFIER = 0x02E1
|
||||
19
homeassistant/components/victron_ble/manifest.json
Normal file
19
homeassistant/components/victron_ble/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"domain": "victron_ble",
|
||||
"name": "Victron BLE",
|
||||
"bluetooth": [
|
||||
{
|
||||
"connectable": false,
|
||||
"manufacturer_data_start": [16],
|
||||
"manufacturer_id": 737
|
||||
}
|
||||
],
|
||||
"codeowners": ["@rajlaud"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/victron_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["victron-ble-ha-parser==0.4.9"]
|
||||
}
|
||||
85
homeassistant/components/victron_ble/quality_scale.yaml
Normal file
85
homeassistant/components/victron_ble/quality_scale.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
appropriate-polling:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not poll.
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
There is nothing to test, the integration just passively receives BLE advertisements.
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: This integration does not provide additional actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: No options to configure
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates:
|
||||
status: done
|
||||
reauthentication-flow:
|
||||
status: todo
|
||||
test-coverage: done
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use IP addresses. Bluetooth MAC addresses do not change.
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration has a fixed single device per instance, and each device needs a user-supplied encryption key to set up.
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration has a fixed single device.
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
474
homeassistant/components/victron_ble/sensor.py
Normal file
474
homeassistant/components/victron_ble/sensor.py
Normal file
@@ -0,0 +1,474 @@
|
||||
"""Sensor platform for Victron BLE."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from sensor_state_data import DeviceKey
|
||||
from victron_ble_ha_parser import Keys, Units
|
||||
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothEntityKey,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AC_IN_OPTIONS = [
|
||||
"ac_in_1",
|
||||
"ac_in_2",
|
||||
"not_connected",
|
||||
]
|
||||
|
||||
ALARM_OPTIONS = [
|
||||
"low_voltage",
|
||||
"high_voltage",
|
||||
"low_soc",
|
||||
"low_starter_voltage",
|
||||
"high_starter_voltage",
|
||||
"low_temperature",
|
||||
"high_temperature",
|
||||
"mid_voltage",
|
||||
"overload",
|
||||
"dc_ripple",
|
||||
"low_v_ac_out",
|
||||
"high_v_ac_out",
|
||||
"short_circuit",
|
||||
"bms_lockout",
|
||||
]
|
||||
|
||||
CHARGER_ERROR_OPTIONS = [
|
||||
"no_error",
|
||||
"temperature_battery_high",
|
||||
"voltage_high",
|
||||
"remote_temperature_auto_reset",
|
||||
"remote_temperature_not_auto_reset",
|
||||
"remote_battery",
|
||||
"high_ripple",
|
||||
"temperature_battery_low",
|
||||
"temperature_charger",
|
||||
"over_current",
|
||||
"bulk_time",
|
||||
"current_sensor",
|
||||
"internal_temperature",
|
||||
"fan",
|
||||
"overheated",
|
||||
"short_circuit",
|
||||
"converter_issue",
|
||||
"over_charge",
|
||||
"input_voltage",
|
||||
"input_current",
|
||||
"input_power",
|
||||
"input_shutdown_voltage",
|
||||
"input_shutdown_current",
|
||||
"input_shutdown_failure",
|
||||
"inverter_shutdown_pv_isolation",
|
||||
"inverter_shutdown_ground_fault",
|
||||
"inverter_overload",
|
||||
"inverter_temperature",
|
||||
"inverter_peak_current",
|
||||
"inverter_output_voltage",
|
||||
"inverter_self_test",
|
||||
"inverter_ac",
|
||||
"communication",
|
||||
"synchronisation",
|
||||
"bms",
|
||||
"network",
|
||||
"pv_input_shutdown",
|
||||
"cpu_temperature",
|
||||
"calibration_lost",
|
||||
"firmware",
|
||||
"settings",
|
||||
"tester_fail",
|
||||
"internal_dc_voltage",
|
||||
"self_test",
|
||||
"internal_supply",
|
||||
]
|
||||
|
||||
|
||||
def error_to_state(value: float | str | None) -> str | None:
|
||||
"""Convert error code to state string."""
|
||||
value_map: dict[Any, str] = {
|
||||
"internal_supply_a": "internal_supply",
|
||||
"internal_supply_b": "internal_supply",
|
||||
"internal_supply_c": "internal_supply",
|
||||
"internal_supply_d": "internal_supply",
|
||||
"inverter_shutdown_41": "inverter_shutdown_pv_isolation",
|
||||
"inverter_shutdown_42": "inverter_shutdown_pv_isolation",
|
||||
"inverter_shutdown_43": "inverter_shutdown_ground_fault",
|
||||
"internal_temperature_a": "internal_temperature",
|
||||
"internal_temperature_b": "internal_temperature",
|
||||
"inverter_output_voltage_a": "inverter_output_voltage",
|
||||
"inverter_output_voltage_b": "inverter_output_voltage",
|
||||
"internal_dc_voltage_a": "internal_dc_voltage",
|
||||
"internal_dc_voltage_b": "internal_dc_voltage",
|
||||
"remote_temperature_a": "remote_temperature_auto_reset",
|
||||
"remote_temperature_b": "remote_temperature_auto_reset",
|
||||
"remote_temperature_c": "remote_temperature_not_auto_reset",
|
||||
"remote_battery_a": "remote_battery",
|
||||
"remote_battery_b": "remote_battery",
|
||||
"remote_battery_c": "remote_battery",
|
||||
"pv_input_shutdown_80": "pv_input_shutdown",
|
||||
"pv_input_shutdown_81": "pv_input_shutdown",
|
||||
"pv_input_shutdown_82": "pv_input_shutdown",
|
||||
"pv_input_shutdown_83": "pv_input_shutdown",
|
||||
"pv_input_shutdown_84": "pv_input_shutdown",
|
||||
"pv_input_shutdown_85": "pv_input_shutdown",
|
||||
"pv_input_shutdown_86": "pv_input_shutdown",
|
||||
"pv_input_shutdown_87": "pv_input_shutdown",
|
||||
"inverter_self_test_a": "inverter_self_test",
|
||||
"inverter_self_test_b": "inverter_self_test",
|
||||
"inverter_self_test_c": "inverter_self_test",
|
||||
"network_a": "network",
|
||||
"network_b": "network",
|
||||
"network_c": "network",
|
||||
"network_d": "network",
|
||||
}
|
||||
return value_map.get(value)
|
||||
|
||||
|
||||
DEVICE_STATE_OPTIONS = [
|
||||
"off",
|
||||
"low_power",
|
||||
"fault",
|
||||
"bulk",
|
||||
"absorption",
|
||||
"float",
|
||||
"storage",
|
||||
"equalize_manual",
|
||||
"inverting",
|
||||
"power_supply",
|
||||
"starting_up",
|
||||
"repeated_absorption",
|
||||
"recondition",
|
||||
"battery_safe",
|
||||
"active",
|
||||
"external_control",
|
||||
"not_available",
|
||||
]
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class VictronBLESensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Victron BLE sensor entity."""
|
||||
|
||||
value_fn: Callable[[float | int | str | None], float | int | str | None] = (
|
||||
lambda x: x
|
||||
)
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS = {
|
||||
Keys.AC_IN_POWER: VictronBLESensorEntityDescription(
|
||||
key=Keys.AC_IN_POWER,
|
||||
translation_key=Keys.AC_IN_POWER,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.AC_IN_STATE: VictronBLESensorEntityDescription(
|
||||
key=Keys.AC_IN_STATE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="ac_in_state",
|
||||
options=AC_IN_OPTIONS,
|
||||
),
|
||||
Keys.AC_OUT_POWER: VictronBLESensorEntityDescription(
|
||||
key=Keys.AC_OUT_POWER,
|
||||
translation_key=Keys.AC_OUT_POWER,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.AC_OUT_STATE: VictronBLESensorEntityDescription(
|
||||
key=Keys.AC_OUT_STATE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="device_state",
|
||||
options=DEVICE_STATE_OPTIONS,
|
||||
),
|
||||
Keys.ALARM: VictronBLESensorEntityDescription(
|
||||
key=Keys.ALARM,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="alarm",
|
||||
options=ALARM_OPTIONS,
|
||||
),
|
||||
Keys.BALANCER_STATUS: VictronBLESensorEntityDescription(
|
||||
key=Keys.BALANCER_STATUS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="balancer_status",
|
||||
options=["balanced", "balancing", "imbalance"],
|
||||
),
|
||||
Keys.BATTERY_CURRENT: VictronBLESensorEntityDescription(
|
||||
key=Keys.BATTERY_CURRENT,
|
||||
translation_key=Keys.BATTERY_CURRENT,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.BATTERY_TEMPERATURE: VictronBLESensorEntityDescription(
|
||||
key=Keys.BATTERY_TEMPERATURE,
|
||||
translation_key=Keys.BATTERY_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.BATTERY_VOLTAGE: VictronBLESensorEntityDescription(
|
||||
key=Keys.BATTERY_VOLTAGE,
|
||||
translation_key=Keys.BATTERY_VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.CHARGER_ERROR: VictronBLESensorEntityDescription(
|
||||
key=Keys.CHARGER_ERROR,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="charger_error",
|
||||
options=CHARGER_ERROR_OPTIONS,
|
||||
value_fn=error_to_state,
|
||||
),
|
||||
Keys.CONSUMED_AMPERE_HOURS: VictronBLESensorEntityDescription(
|
||||
key=Keys.CONSUMED_AMPERE_HOURS,
|
||||
translation_key=Keys.CONSUMED_AMPERE_HOURS,
|
||||
native_unit_of_measurement=Units.ELECTRIC_CURRENT_FLOW_AMPERE_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.CURRENT: VictronBLESensorEntityDescription(
|
||||
key=Keys.CURRENT,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.DEVICE_STATE: VictronBLESensorEntityDescription(
|
||||
key=Keys.DEVICE_STATE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="device_state",
|
||||
options=DEVICE_STATE_OPTIONS,
|
||||
),
|
||||
Keys.ERROR_CODE: VictronBLESensorEntityDescription(
|
||||
key=Keys.ERROR_CODE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="charger_error",
|
||||
options=CHARGER_ERROR_OPTIONS,
|
||||
),
|
||||
Keys.EXTERNAL_DEVICE_LOAD: VictronBLESensorEntityDescription(
|
||||
key=Keys.EXTERNAL_DEVICE_LOAD,
|
||||
translation_key=Keys.EXTERNAL_DEVICE_LOAD,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.INPUT_VOLTAGE: VictronBLESensorEntityDescription(
|
||||
key=Keys.INPUT_VOLTAGE,
|
||||
translation_key=Keys.INPUT_VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.METER_TYPE: VictronBLESensorEntityDescription(
|
||||
key=Keys.METER_TYPE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="meter_type",
|
||||
options=[
|
||||
"solar_charger",
|
||||
"wind_charger",
|
||||
"shaft_generator",
|
||||
"alternator",
|
||||
"fuel_cell",
|
||||
"water_generator",
|
||||
"dc_dc_charger",
|
||||
"ac_charger",
|
||||
"generic_source",
|
||||
"generic_load",
|
||||
"electric_drive",
|
||||
"fridge",
|
||||
"water_pump",
|
||||
"bilge_pump",
|
||||
"dc_system",
|
||||
"inverter",
|
||||
"water_heater",
|
||||
],
|
||||
),
|
||||
Keys.MIDPOINT_VOLTAGE: VictronBLESensorEntityDescription(
|
||||
key=Keys.MIDPOINT_VOLTAGE,
|
||||
translation_key=Keys.MIDPOINT_VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.OFF_REASON: VictronBLESensorEntityDescription(
|
||||
key=Keys.OFF_REASON,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="off_reason",
|
||||
options=[
|
||||
"no_reason",
|
||||
"no_input_power",
|
||||
"switched_off_switch",
|
||||
"switched_off_register",
|
||||
"remote_input",
|
||||
"protection_active",
|
||||
"pay_as_you_go_out_of_credit",
|
||||
"bms",
|
||||
"engine_shutdown",
|
||||
"analysing_input_voltage",
|
||||
],
|
||||
),
|
||||
Keys.OUTPUT_VOLTAGE: VictronBLESensorEntityDescription(
|
||||
key=Keys.OUTPUT_VOLTAGE,
|
||||
translation_key=Keys.OUTPUT_VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.REMAINING_MINUTES: VictronBLESensorEntityDescription(
|
||||
key=Keys.REMAINING_MINUTES,
|
||||
translation_key=Keys.REMAINING_MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorDeviceClass.SIGNAL_STRENGTH: VictronBLESensorEntityDescription(
|
||||
key=SensorDeviceClass.SIGNAL_STRENGTH.value,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.SOLAR_POWER: VictronBLESensorEntityDescription(
|
||||
key=Keys.SOLAR_POWER,
|
||||
translation_key=Keys.SOLAR_POWER,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.STARTER_VOLTAGE: VictronBLESensorEntityDescription(
|
||||
key=Keys.STARTER_VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.STATE_OF_CHARGE: VictronBLESensorEntityDescription(
|
||||
key=Keys.STATE_OF_CHARGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.TEMPERATURE: VictronBLESensorEntityDescription(
|
||||
key=Keys.TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.VOLTAGE: VictronBLESensorEntityDescription(
|
||||
key=Keys.VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
Keys.WARNING: VictronBLESensorEntityDescription(
|
||||
key=Keys.WARNING,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="alarm",
|
||||
options=ALARM_OPTIONS,
|
||||
),
|
||||
Keys.YIELD_TODAY: VictronBLESensorEntityDescription(
|
||||
key=Keys.YIELD_TODAY,
|
||||
translation_key=Keys.YIELD_TODAY,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
}
|
||||
|
||||
for i in range(1, 8):
|
||||
cell_key = getattr(Keys, f"CELL_{i}_VOLTAGE")
|
||||
SENSOR_DESCRIPTIONS[cell_key] = VictronBLESensorEntityDescription(
|
||||
key=cell_key,
|
||||
translation_key="cell_voltage",
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
)
|
||||
|
||||
|
||||
def _device_key_to_bluetooth_entity_key(
|
||||
device_key: DeviceKey,
|
||||
) -> PassiveBluetoothEntityKey:
|
||||
"""Convert a device key to an entity key."""
|
||||
return PassiveBluetoothEntityKey(device_key.key, device_key.device_id)
|
||||
|
||||
|
||||
def sensor_update_to_bluetooth_data_update(
|
||||
sensor_update,
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Convert a sensor update to a bluetooth data update."""
|
||||
return PassiveBluetoothDataUpdate(
|
||||
devices={
|
||||
device_id: sensor_device_info_to_hass_device_info(device_info)
|
||||
for device_id, device_info in sensor_update.devices.items()
|
||||
},
|
||||
entity_descriptions={
|
||||
_device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[
|
||||
device_key.key
|
||||
]
|
||||
for device_key in sensor_update.entity_descriptions
|
||||
if device_key.key in SENSOR_DESCRIPTIONS
|
||||
},
|
||||
entity_data={
|
||||
_device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value
|
||||
for device_key, sensor_values in sensor_update.entity_values.items()
|
||||
if device_key.key in SENSOR_DESCRIPTIONS
|
||||
},
|
||||
entity_names={},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Victron BLE sensor."""
|
||||
coordinator = entry.runtime_data
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
VictronBLESensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class VictronBLESensorEntity(PassiveBluetoothProcessorEntity, SensorEntity):
|
||||
"""Representation of Victron BLE sensor."""
|
||||
|
||||
entity_description: VictronBLESensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | int | str | None:
|
||||
"""Return the state of the sensor."""
|
||||
value = self.processor.entity_data.get(self.entity_key)
|
||||
|
||||
return self.entity_description.value_fn(value)
|
||||
234
homeassistant/components/victron_ble/strings.json
Normal file
234
homeassistant/components/victron_ble/strings.json
Normal file
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"common": {
|
||||
"high_voltage": "High voltage",
|
||||
"low_voltage": "Low voltage",
|
||||
"midpoint_voltage": "Midpoint voltage",
|
||||
"starter_voltage": "Starter voltage"
|
||||
},
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"invalid_access_token": "Invalid encryption key for instant readout",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||
},
|
||||
"flow_title": "{title}",
|
||||
"step": {
|
||||
"access_token": {
|
||||
"data": {
|
||||
"access_token": "The encryption key for instant readout of the Victron device."
|
||||
},
|
||||
"data_description": {
|
||||
"access_token": "The encryption key for instant readout may be found in the VictronConnect app under Settings > Product info > Instant readout details > Encryption data > Encryption Key."
|
||||
},
|
||||
"title": "{title}"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"address": "The Bluetooth address of the Victron device."
|
||||
},
|
||||
"data_description": {
|
||||
"address": "This Bluetooth address is automatically discovered. You may view a device's Bluetooth address in the VictronConnect app under Settings > Product info > Instant readout details > Encryption data > MAC Address."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"ac_in_power": {
|
||||
"name": "AC-in power"
|
||||
},
|
||||
"ac_in_state": {
|
||||
"name": "AC-in state",
|
||||
"state": {
|
||||
"ac_in_1": "AC-in 1",
|
||||
"ac_in_2": "AC-in 2",
|
||||
"not_connected": "Not connected"
|
||||
}
|
||||
},
|
||||
"ac_out_power": {
|
||||
"name": "AC-out power"
|
||||
},
|
||||
"alarm": {
|
||||
"name": "Alarm",
|
||||
"state": {
|
||||
"bms_lockout": "Battery management system lockout",
|
||||
"dc_ripple": "DC ripple",
|
||||
"high_starter_voltage": "High starter voltage",
|
||||
"high_temperature": "High temperature",
|
||||
"high_v_ac_out": "AC-out overvoltage",
|
||||
"high_voltage": "Overvoltage",
|
||||
"low_soc": "Low state of charge",
|
||||
"low_starter_voltage": "Low starter voltage",
|
||||
"low_temperature": "Low temperature",
|
||||
"low_v_ac_out": "AC-out undervoltage",
|
||||
"low_voltage": "Undervoltage",
|
||||
"mid_voltage": "[%key:component::victron_ble::common::midpoint_voltage%]",
|
||||
"overload": "Overload",
|
||||
"short_circuit": "Short circuit"
|
||||
}
|
||||
},
|
||||
"balancer_status": {
|
||||
"name": "Balancer status",
|
||||
"state": {
|
||||
"balanced": "Balanced",
|
||||
"balancing": "Balancing",
|
||||
"imbalance": "Imbalance"
|
||||
}
|
||||
},
|
||||
"battery_current": {
|
||||
"name": "Battery current"
|
||||
},
|
||||
"battery_temperature": {
|
||||
"name": "Battery temperature"
|
||||
},
|
||||
"battery_voltage": {
|
||||
"name": "Battery voltage"
|
||||
},
|
||||
"cell_voltage": {
|
||||
"name": "Cell {cell} voltage"
|
||||
},
|
||||
"charger_error": {
|
||||
"name": "Charger error",
|
||||
"state": {
|
||||
"bms": "BMS connection lost",
|
||||
"bulk_time": "Bulk time limit exceeded",
|
||||
"calibration_lost": "Factory calibration data lost",
|
||||
"communication": "Communication warning",
|
||||
"converter_issue": "Converter issue",
|
||||
"cpu_temperature": "CPU temperature too high",
|
||||
"current_sensor": "Current sensor issue",
|
||||
"fan": "Fan failure",
|
||||
"firmware": "Invalid or incompatible firmware",
|
||||
"high_ripple": "Battery high ripple voltage",
|
||||
"input_current": "Input overcurrent",
|
||||
"input_power": "Input overpower",
|
||||
"input_shutdown_current": "Input shutdown (current flow during off mode)",
|
||||
"input_shutdown_failure": "PV input failed to shutdown",
|
||||
"input_shutdown_voltage": "Input shutdown (battery overvoltage)",
|
||||
"input_voltage": "Input overvoltage",
|
||||
"internal_dc_voltage": "Internal DC voltage error",
|
||||
"internal_supply": "Internal supply voltage error",
|
||||
"internal_temperature": "Internal temperature sensor failure",
|
||||
"inverter_ac": "Inverter AC voltage on output",
|
||||
"inverter_output_voltage": "Inverter output voltage",
|
||||
"inverter_overload": "Inverter overload",
|
||||
"inverter_peak_current": "Inverter peak current",
|
||||
"inverter_self_test": "Inverter self-test failed",
|
||||
"inverter_shutdown_ground_fault": "Inverter shutdown (Ground fault)",
|
||||
"inverter_shutdown_pv_isolation": "Inverter shutdown (PV isolation)",
|
||||
"inverter_temperature": "Inverter temperature too high",
|
||||
"network": "Network misconfigured",
|
||||
"no_error": "No error",
|
||||
"over_charge": "Overcharge protection",
|
||||
"over_current": "Charger overcurrent",
|
||||
"overheated": "Terminals overheated",
|
||||
"pv_input_shutdown": "PV input shutdown",
|
||||
"remote_battery": "Remote battery voltage sense failure",
|
||||
"remote_temperature_auto_reset": "Remote temperature sensor failure (auto-reset)",
|
||||
"remote_temperature_not_auto_reset": "Remote temperature sensor failure (not auto-reset)",
|
||||
"self_test": "PV residual current sensor self-test failure",
|
||||
"settings": "Settings data lost",
|
||||
"short_circuit": "Charger short circuit",
|
||||
"synchronisation": "Synchronized charging device configuration issue",
|
||||
"temperature_battery_high": "Battery temperature too high",
|
||||
"temperature_battery_low": "Battery temperature too low",
|
||||
"temperature_charger": "Charger temperature too high",
|
||||
"tester_fail": "Tester fail",
|
||||
"voltage_high": "Battery overvoltage"
|
||||
}
|
||||
},
|
||||
"consumed_ampere_hours": {
|
||||
"name": "Consumed ampere hours"
|
||||
},
|
||||
"device_state": {
|
||||
"name": "Device state",
|
||||
"state": {
|
||||
"absorption": "Absorption",
|
||||
"active": "Active",
|
||||
"battery_safe": "Battery safe",
|
||||
"bulk": "Bulk",
|
||||
"equalize_manual": "Equalize (manual)",
|
||||
"external_control": "External control",
|
||||
"fault": "Fault",
|
||||
"float": "Float",
|
||||
"inverting": "Inverting",
|
||||
"low_power": "Low power",
|
||||
"not_available": "Not available",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"power_supply": "Power supply",
|
||||
"recondition": "Recondition",
|
||||
"repeated_absorption": "Repeated absorption",
|
||||
"starting_up": "Starting up",
|
||||
"storage": "Storage"
|
||||
}
|
||||
},
|
||||
"error_code": {
|
||||
"name": "Error code"
|
||||
},
|
||||
"external_device_load": {
|
||||
"name": "External device load"
|
||||
},
|
||||
"input_voltage": {
|
||||
"name": "Input voltage"
|
||||
},
|
||||
"meter_type": {
|
||||
"name": "Meter type",
|
||||
"state": {
|
||||
"ac_charger": "AC charger",
|
||||
"alternator": "Alternator",
|
||||
"bilge_pump": "Bilge pump",
|
||||
"dc_dc_charger": "DC-DC charger",
|
||||
"dc_system": "DC system",
|
||||
"electric_drive": "Electric drive",
|
||||
"fridge": "Fridge",
|
||||
"fuel_cell": "Fuel cell",
|
||||
"generic_load": "Generic load",
|
||||
"generic_source": "Generic source",
|
||||
"inverter": "Inverter",
|
||||
"shaft_generator": "Shaft generator",
|
||||
"solar_charger": "Solar charger",
|
||||
"water_generator": "Water generator",
|
||||
"water_heater": "Water heater",
|
||||
"water_pump": "Water pump",
|
||||
"wind_charger": "Wind charger"
|
||||
}
|
||||
},
|
||||
"midpoint_voltage": {
|
||||
"name": "[%key:component::victron_ble::common::midpoint_voltage%]"
|
||||
},
|
||||
"off_reason": {
|
||||
"name": "Off reason",
|
||||
"state": {
|
||||
"analysing_input_voltage": "Analyzing input voltage",
|
||||
"bms": "Battery management system",
|
||||
"engine_shutdown": "Engine shutdown",
|
||||
"no_input_power": "No input power",
|
||||
"no_reason": "No reason",
|
||||
"pay_as_you_go_out_of_credit": "Pay-as-you-go out of credit",
|
||||
"protection_active": "Protection active",
|
||||
"remote_input": "Remote input",
|
||||
"switched_off_register": "Switched off by register",
|
||||
"switched_off_switch": "Switched off by switch"
|
||||
}
|
||||
},
|
||||
"output_voltage": {
|
||||
"name": "Output voltage"
|
||||
},
|
||||
"remaining_minutes": {
|
||||
"name": "Remaining minutes"
|
||||
},
|
||||
"solar_power": {
|
||||
"name": "Solar power"
|
||||
},
|
||||
"starter_voltage": {
|
||||
"name": "[%key:component::victron_ble::common::starter_voltage%]"
|
||||
},
|
||||
"warning": {
|
||||
"name": "Warning"
|
||||
},
|
||||
"yield_today": {
|
||||
"name": "Yield today"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
homeassistant/generated/bluetooth.py
generated
8
homeassistant/generated/bluetooth.py
generated
@@ -849,6 +849,14 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
|
||||
"manufacturer_id": 34714,
|
||||
"service_uuid": "0000cee0-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
"connectable": False,
|
||||
"domain": "victron_ble",
|
||||
"manufacturer_data_start": [
|
||||
16,
|
||||
],
|
||||
"manufacturer_id": 737,
|
||||
},
|
||||
{
|
||||
"connectable": False,
|
||||
"domain": "xiaomi_ble",
|
||||
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -723,6 +723,7 @@ FLOWS = {
|
||||
"version",
|
||||
"vesync",
|
||||
"vicare",
|
||||
"victron_ble",
|
||||
"victron_remote_monitoring",
|
||||
"vilfo",
|
||||
"vizio",
|
||||
|
||||
@@ -7282,11 +7282,22 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"victron_remote_monitoring": {
|
||||
"name": "Victron Remote Monitoring",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
"victron": {
|
||||
"name": "Victron",
|
||||
"integrations": {
|
||||
"victron_ble": {
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "Victron BLE"
|
||||
},
|
||||
"victron_remote_monitoring": {
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Victron Remote Monitoring"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vilfo": {
|
||||
"name": "Vilfo Router",
|
||||
|
||||
@@ -68,8 +68,8 @@ class AreasRegistryStoreData(TypedDict):
|
||||
class EventAreaRegistryUpdatedData(TypedDict):
|
||||
"""EventAreaRegistryUpdated data."""
|
||||
|
||||
action: Literal["create", "remove", "update"]
|
||||
area_id: str
|
||||
action: Literal["create", "remove", "update", "reorder"]
|
||||
area_id: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True, slots=True)
|
||||
@@ -420,6 +420,26 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
|
||||
self.async_schedule_save()
|
||||
return new
|
||||
|
||||
@callback
|
||||
def async_reorder(self, area_ids: list[str]) -> None:
|
||||
"""Reorder areas."""
|
||||
self.hass.verify_event_loop_thread("area_registry.async_reorder")
|
||||
|
||||
if set(area_ids) != set(self.areas.data.keys()):
|
||||
raise ValueError(
|
||||
"The area_ids list must contain all existing area IDs exactly once"
|
||||
)
|
||||
|
||||
reordered_data = {area_id: self.areas.data[area_id] for area_id in area_ids}
|
||||
self.areas.data.clear()
|
||||
self.areas.data.update(reordered_data)
|
||||
|
||||
self.async_schedule_save()
|
||||
self.hass.bus.async_fire_internal(
|
||||
EVENT_AREA_REGISTRY_UPDATED,
|
||||
EventAreaRegistryUpdatedData(action="reorder", area_id=None),
|
||||
)
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load the area registry."""
|
||||
self._async_setup_cleanup()
|
||||
@@ -489,7 +509,9 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
|
||||
@callback
|
||||
def _handle_floor_registry_update(event: fr.EventFloorRegistryUpdated) -> None:
|
||||
"""Update areas that are associated with a floor that has been removed."""
|
||||
floor_id = event.data["floor_id"]
|
||||
floor_id = event.data.get("floor_id")
|
||||
if floor_id is None:
|
||||
return
|
||||
for area in self.areas.get_areas_for_floor(floor_id):
|
||||
self.async_update(area.id, floor_id=None)
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@ class FloorRegistryStoreData(TypedDict):
|
||||
class EventFloorRegistryUpdatedData(TypedDict):
|
||||
"""Event data for when the floor registry is updated."""
|
||||
|
||||
action: Literal["create", "remove", "update"]
|
||||
floor_id: str
|
||||
action: Literal["create", "remove", "update", "reorder"]
|
||||
floor_id: str | None
|
||||
|
||||
|
||||
type EventFloorRegistryUpdated = Event[EventFloorRegistryUpdatedData]
|
||||
@@ -261,6 +261,28 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]):
|
||||
|
||||
return new
|
||||
|
||||
@callback
|
||||
def async_reorder(self, floor_ids: list[str]) -> None:
|
||||
"""Reorder floors."""
|
||||
self.hass.verify_event_loop_thread("floor_registry.async_reorder")
|
||||
|
||||
if set(floor_ids) != set(self.floors.data.keys()):
|
||||
raise ValueError(
|
||||
"The floor_ids list must contain all existing floor IDs exactly once"
|
||||
)
|
||||
|
||||
reordered_data = {
|
||||
floor_id: self.floors.data[floor_id] for floor_id in floor_ids
|
||||
}
|
||||
self.floors.data.clear()
|
||||
self.floors.data.update(reordered_data)
|
||||
|
||||
self.async_schedule_save()
|
||||
self.hass.bus.async_fire_internal(
|
||||
EVENT_FLOOR_REGISTRY_UPDATED,
|
||||
EventFloorRegistryUpdatedData(action="reorder", floor_id=None),
|
||||
)
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load the floor registry."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
21
requirements_all.txt
generated
21
requirements_all.txt
generated
@@ -80,7 +80,7 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.sunricher_dali
|
||||
PySrDaliGateway==0.13.1
|
||||
PySrDaliGateway==0.16.2
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.73.0
|
||||
@@ -209,7 +209,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.5.1
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2.7.0
|
||||
aioautomower==2.7.1
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.2
|
||||
@@ -389,7 +389,7 @@ aioruuvigateway==0.1.0
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==13.17.0
|
||||
aioshelly==13.19.0
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@@ -422,7 +422,7 @@ aiotedee==0.2.25
|
||||
aiotractive==0.6.0
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==87
|
||||
aiounifi==88
|
||||
|
||||
# homeassistant.components.usb
|
||||
aiousbwatcher==1.1.1
|
||||
@@ -1601,7 +1601,7 @@ odp-amsterdam==6.1.2
|
||||
oemthermostat==1.1.1
|
||||
|
||||
# homeassistant.components.ohme
|
||||
ohme==1.5.2
|
||||
ohme==1.6.0
|
||||
|
||||
# homeassistant.components.ollama
|
||||
ollama==0.5.1
|
||||
@@ -1613,7 +1613,7 @@ omnilogic==0.4.5
|
||||
ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
onedrive-personal-sdk==0.0.16
|
||||
onedrive-personal-sdk==0.0.17
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
@@ -2150,7 +2150,7 @@ pylitejet==0.6.3
|
||||
pylitterbot==2025.0.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.25.0
|
||||
pylutron-caseta==0.26.0
|
||||
|
||||
# homeassistant.components.lutron
|
||||
pylutron==0.2.18
|
||||
@@ -2269,7 +2269,7 @@ pypaperless==4.1.1
|
||||
pypca==0.0.7
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.9.3
|
||||
pypck==0.9.5
|
||||
|
||||
# homeassistant.components.pglab
|
||||
pypglab==0.0.5
|
||||
@@ -3054,7 +3054,7 @@ unifi_ap==0.0.2
|
||||
unifiled==0.11
|
||||
|
||||
# homeassistant.components.homeassistant_hardware
|
||||
universal-silabs-flasher==0.1.0
|
||||
universal-silabs-flasher==0.1.2
|
||||
|
||||
# homeassistant.components.upb
|
||||
upb-lib==0.6.1
|
||||
@@ -3088,6 +3088,9 @@ velbus-aio==2025.11.0
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.21
|
||||
|
||||
# homeassistant.components.victron_ble
|
||||
victron-ble-ha-parser==0.4.9
|
||||
|
||||
# homeassistant.components.victron_remote_monitoring
|
||||
victron-vrm==0.1.8
|
||||
|
||||
|
||||
21
requirements_test_all.txt
generated
21
requirements_test_all.txt
generated
@@ -77,7 +77,7 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.sunricher_dali
|
||||
PySrDaliGateway==0.13.1
|
||||
PySrDaliGateway==0.16.2
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.73.0
|
||||
@@ -197,7 +197,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.5.1
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2.7.0
|
||||
aioautomower==2.7.1
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.2
|
||||
@@ -371,7 +371,7 @@ aioruuvigateway==0.1.0
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==13.17.0
|
||||
aioshelly==13.19.0
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@@ -404,7 +404,7 @@ aiotedee==0.2.25
|
||||
aiotractive==0.6.0
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==87
|
||||
aiounifi==88
|
||||
|
||||
# homeassistant.components.usb
|
||||
aiousbwatcher==1.1.1
|
||||
@@ -1372,7 +1372,7 @@ objgraph==3.5.0
|
||||
odp-amsterdam==6.1.2
|
||||
|
||||
# homeassistant.components.ohme
|
||||
ohme==1.5.2
|
||||
ohme==1.6.0
|
||||
|
||||
# homeassistant.components.ollama
|
||||
ollama==0.5.1
|
||||
@@ -1384,7 +1384,7 @@ omnilogic==0.4.5
|
||||
ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
onedrive-personal-sdk==0.0.16
|
||||
onedrive-personal-sdk==0.0.17
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
@@ -1794,7 +1794,7 @@ pylitejet==0.6.3
|
||||
pylitterbot==2025.0.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.25.0
|
||||
pylutron-caseta==0.26.0
|
||||
|
||||
# homeassistant.components.lutron
|
||||
pylutron==0.2.18
|
||||
@@ -1892,7 +1892,7 @@ pypalazzetti==0.1.20
|
||||
pypaperless==4.1.1
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.9.3
|
||||
pypck==0.9.5
|
||||
|
||||
# homeassistant.components.pglab
|
||||
pypglab==0.0.5
|
||||
@@ -2521,7 +2521,7 @@ ultraheat-api==0.5.7
|
||||
unifi-discovery==1.2.0
|
||||
|
||||
# homeassistant.components.homeassistant_hardware
|
||||
universal-silabs-flasher==0.1.0
|
||||
universal-silabs-flasher==0.1.2
|
||||
|
||||
# homeassistant.components.upb
|
||||
upb-lib==0.6.1
|
||||
@@ -2555,6 +2555,9 @@ velbus-aio==2025.11.0
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.21
|
||||
|
||||
# homeassistant.components.victron_ble
|
||||
victron-ble-ha-parser==0.4.9
|
||||
|
||||
# homeassistant.components.victron_remote_monitoring
|
||||
victron-vrm==0.1.8
|
||||
|
||||
|
||||
@@ -1608,12 +1608,16 @@ def mock_integration(
|
||||
top_level_files: set[str] | None = None,
|
||||
) -> loader.Integration:
|
||||
"""Mock an integration."""
|
||||
integration = loader.Integration(
|
||||
hass,
|
||||
path = (
|
||||
f"{loader.PACKAGE_BUILTIN}.{module.DOMAIN}"
|
||||
if built_in
|
||||
else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}",
|
||||
pathlib.Path(""),
|
||||
else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}"
|
||||
)
|
||||
|
||||
integration = loader.Integration(
|
||||
hass,
|
||||
path,
|
||||
pathlib.Path(path.replace(".", "/")),
|
||||
module.mock_manifest(),
|
||||
top_level_files,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The tests for the analytics ."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
@@ -22,8 +23,10 @@ from homeassistant.components.analytics.analytics import (
|
||||
from homeassistant.components.analytics.const import (
|
||||
ANALYTICS_ENDPOINT_URL,
|
||||
ANALYTICS_ENDPOINT_URL_DEV,
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
|
||||
ATTR_BASE,
|
||||
ATTR_DIAGNOSTICS,
|
||||
ATTR_SNAPSHOTS,
|
||||
ATTR_STATISTICS,
|
||||
ATTR_USAGE,
|
||||
)
|
||||
@@ -31,13 +34,20 @@ from homeassistant.components.number import NumberDeviceClass
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
|
||||
from homeassistant.const import ATTR_ASSUMED_STATE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, ReleaseChannel
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.loader import IntegrationNotFound
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry, MockModule, mock_integration, mock_platform
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
MockModule,
|
||||
async_fire_time_changed,
|
||||
mock_integration,
|
||||
mock_platform,
|
||||
)
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
@@ -59,9 +69,31 @@ def uuid_mock() -> Generator[None]:
|
||||
@pytest.fixture(autouse=True)
|
||||
def ha_version_mock() -> Generator[None]:
|
||||
"""Mock the core version."""
|
||||
with patch(
|
||||
"homeassistant.components.analytics.analytics.HA_VERSION",
|
||||
MOCK_VERSION,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.analytics.analytics.HA_VERSION",
|
||||
MOCK_VERSION,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.analytics.analytics.RELEASE_CHANNEL",
|
||||
ReleaseChannel.STABLE,
|
||||
),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ha_dev_version_mock() -> Generator[None]:
|
||||
"""Mock the core version as a dev version."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.analytics.analytics.HA_VERSION",
|
||||
MOCK_VERSION_DEV,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.analytics.analytics.RELEASE_CHANNEL",
|
||||
ReleaseChannel.DEV,
|
||||
),
|
||||
):
|
||||
yield
|
||||
|
||||
@@ -97,7 +129,6 @@ async def test_no_send(
|
||||
|
||||
await analytics.send_analytics()
|
||||
|
||||
assert "Nothing to submit" in caplog.text
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
|
||||
@@ -615,7 +646,7 @@ async def test_custom_integrations(
|
||||
assert snapshot == submitted_data
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
@pytest.mark.usefixtures("ha_dev_version_mock", "supervisor_client")
|
||||
async def test_dev_url(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
@@ -625,16 +656,13 @@ async def test_dev_url(
|
||||
analytics = Analytics(hass)
|
||||
await analytics.save_preferences({ATTR_BASE: True})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION_DEV
|
||||
):
|
||||
await analytics.send_analytics()
|
||||
await analytics.send_analytics()
|
||||
|
||||
payload = aioclient_mock.mock_calls[0]
|
||||
assert str(payload[1]) == ANALYTICS_ENDPOINT_URL_DEV
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
@pytest.mark.usefixtures("ha_dev_version_mock", "supervisor_client")
|
||||
async def test_dev_url_error(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
@@ -645,10 +673,7 @@ async def test_dev_url_error(
|
||||
analytics = Analytics(hass)
|
||||
await analytics.save_preferences({ATTR_BASE: True})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION_DEV
|
||||
):
|
||||
await analytics.send_analytics()
|
||||
await analytics.send_analytics()
|
||||
|
||||
payload = aioclient_mock.mock_calls[0]
|
||||
assert str(payload[1]) == ANALYTICS_ENDPOINT_URL_DEV
|
||||
@@ -860,7 +885,7 @@ async def test_send_with_problems_loading_yaml(
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_hass_config", "supervisor_client")
|
||||
@pytest.mark.usefixtures("ha_dev_version_mock", "mock_hass_config", "supervisor_client")
|
||||
async def test_timeout_while_sending(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -871,10 +896,7 @@ async def test_timeout_while_sending(
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, exc=TimeoutError())
|
||||
|
||||
await analytics.save_preferences({ATTR_BASE: True})
|
||||
with patch(
|
||||
"homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION_DEV
|
||||
):
|
||||
await analytics.send_analytics()
|
||||
await analytics.send_analytics()
|
||||
|
||||
assert "Timeout sending analytics" in caplog.text
|
||||
|
||||
@@ -1426,3 +1448,346 @@ async def test_analytics_platforms(
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def test_send_snapshot_disabled(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test no snapshots are sent."""
|
||||
analytics = Analytics(hass)
|
||||
|
||||
await analytics.send_snapshot()
|
||||
|
||||
await analytics.save_preferences({ATTR_SNAPSHOTS: False})
|
||||
await analytics.send_snapshot()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_send_snapshot_success(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test successful snapshot submission."""
|
||||
aioclient_mock.post(
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
|
||||
status=200,
|
||||
json={"submission_identifier": "test-identifier-123"},
|
||||
)
|
||||
|
||||
analytics = Analytics(hass)
|
||||
|
||||
await analytics.save_preferences({ATTR_SNAPSHOTS: True})
|
||||
await analytics.send_snapshot()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
|
||||
preferences = await analytics._store.async_load()
|
||||
assert preferences["submission_identifier"] == "test-identifier-123"
|
||||
assert "Submitted snapshot analytics to Home Assistant servers" in caplog.text
|
||||
|
||||
|
||||
async def test_send_snapshot_with_existing_identifier(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test snapshot submission with existing identifier."""
|
||||
aioclient_mock.post(
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
|
||||
status=200,
|
||||
json={"submission_identifier": "test-identifier-123"},
|
||||
)
|
||||
|
||||
analytics = Analytics(hass)
|
||||
with patch(
|
||||
"homeassistant.helpers.storage.Store.async_load",
|
||||
return_value={
|
||||
"onboarded": True,
|
||||
"preferences": {ATTR_BASE: True, ATTR_SNAPSHOTS: True},
|
||||
"uuid": "12345",
|
||||
"submission_identifier": "old-identifier",
|
||||
},
|
||||
):
|
||||
await analytics.load()
|
||||
|
||||
await analytics.send_snapshot()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
call_headers = aioclient_mock.mock_calls[0][3]
|
||||
assert call_headers["X-Device-Database-Submission-Identifier"] == "old-identifier"
|
||||
|
||||
preferences = await analytics._store.async_load()
|
||||
assert preferences["submission_identifier"] == "test-identifier-123"
|
||||
assert "Submitted snapshot analytics to Home Assistant servers" in caplog.text
|
||||
|
||||
|
||||
async def test_send_snapshot_invalid_identifier(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test snapshot submission with invalid identifier."""
|
||||
aioclient_mock.post(
|
||||
ANALYTICS_SNAPSHOT_ENDPOINT_URL,
|
||||
status=400,
|
||||
json={
|
||||
"kind": "invalid-submission-identifier",
|
||||
"message": "The identifier is invalid",
|
||||
},
|
||||
)
|
||||
|
||||
analytics = Analytics(hass)
|
||||
with patch(
|
||||
"homeassistant.helpers.storage.Store.async_load",
|
||||
return_value={
|
||||
"onboarded": True,
|
||||
"preferences": {ATTR_BASE: True, ATTR_SNAPSHOTS: True},
|
||||
"uuid": "12345",
|
||||
"submission_identifier": "invalid-identifier",
|
||||
},
|
||||
):
|
||||
await analytics.load()
|
||||
|
||||
await analytics.send_snapshot()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
|
||||
preferences = await analytics._store.async_load()
|
||||
assert preferences.get("submission_identifier") is None
|
||||
assert "Invalid submission identifier" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("post_kwargs", "expected_log"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"status": 400,
|
||||
"json": {
|
||||
"kind": "malformed-payload",
|
||||
"message": "Invalid payload format",
|
||||
},
|
||||
},
|
||||
"Malformed snapshot analytics submission",
|
||||
),
|
||||
(
|
||||
{"status": 503, "text": "Service Unavailable"},
|
||||
f"Snapshot analytics service {ANALYTICS_SNAPSHOT_ENDPOINT_URL} unavailable",
|
||||
),
|
||||
(
|
||||
{"status": 500},
|
||||
"Unexpected status code 500 when submitting snapshot analytics",
|
||||
),
|
||||
(
|
||||
{"exc": TimeoutError()},
|
||||
"Timeout sending snapshot analytics",
|
||||
),
|
||||
(
|
||||
{"exc": aiohttp.ClientError()},
|
||||
"Error sending snapshot analytics",
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"bad_request",
|
||||
"service_unavailable",
|
||||
"unexpected_status",
|
||||
"timeout",
|
||||
"client_error",
|
||||
],
|
||||
)
|
||||
async def test_send_snapshot_error(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
post_kwargs: dict[str, Any],
|
||||
expected_log: str,
|
||||
) -> None:
|
||||
"""Test snapshot submission error."""
|
||||
aioclient_mock.post(ANALYTICS_SNAPSHOT_ENDPOINT_URL, **post_kwargs)
|
||||
|
||||
analytics = Analytics(hass)
|
||||
with patch(
|
||||
"homeassistant.helpers.storage.Store.async_load",
|
||||
return_value={
|
||||
"onboarded": True,
|
||||
"preferences": {ATTR_BASE: True, ATTR_SNAPSHOTS: True},
|
||||
"uuid": "12345",
|
||||
},
|
||||
):
|
||||
await analytics.load()
|
||||
|
||||
await analytics.send_snapshot()
|
||||
|
||||
assert expected_log in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("ha_dev_version_mock", "supervisor_client")
|
||||
async def test_async_schedule(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test scheduling."""
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, status=200)
|
||||
aioclient_mock.post(ANALYTICS_SNAPSHOT_ENDPOINT_URL, status=200, json={})
|
||||
|
||||
analytics = Analytics(hass)
|
||||
|
||||
# Schedule when not onboarded
|
||||
await analytics.async_schedule()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
# Onboard and enable both
|
||||
await analytics.save_preferences({ATTR_BASE: True, ATTR_SNAPSHOTS: True})
|
||||
|
||||
await analytics.async_schedule()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert any(
|
||||
str(call[1]) == ANALYTICS_ENDPOINT_URL_DEV for call in aioclient_mock.mock_calls
|
||||
)
|
||||
assert any(
|
||||
str(call[1]) == ANALYTICS_SNAPSHOT_ENDPOINT_URL
|
||||
for call in aioclient_mock.mock_calls
|
||||
)
|
||||
|
||||
preferences = await analytics._store.async_load()
|
||||
assert preferences["snapshot_submission_time"] is not None
|
||||
assert 0 <= preferences["snapshot_submission_time"] <= 86400
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("ha_dev_version_mock")
|
||||
async def test_async_schedule_disabled(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test scheduling when disabled."""
|
||||
analytics = Analytics(hass)
|
||||
with patch(
|
||||
"homeassistant.helpers.storage.Store.async_load",
|
||||
return_value={
|
||||
"onboarded": True,
|
||||
"preferences": {ATTR_BASE: False, ATTR_SNAPSHOTS: False},
|
||||
"uuid": "12345",
|
||||
},
|
||||
):
|
||||
await analytics.load()
|
||||
|
||||
await analytics.async_schedule()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
async def test_async_schedule_snapshots_not_dev(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test that snapshots are not scheduled on non-dev versions."""
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
|
||||
|
||||
analytics = Analytics(hass)
|
||||
with patch(
|
||||
"homeassistant.helpers.storage.Store.async_load",
|
||||
return_value={
|
||||
"onboarded": True,
|
||||
"preferences": {ATTR_BASE: True, ATTR_SNAPSHOTS: True},
|
||||
"uuid": "12345",
|
||||
},
|
||||
):
|
||||
await analytics.load()
|
||||
|
||||
await analytics.async_schedule()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
assert str(aioclient_mock.mock_calls[0][1]) == ANALYTICS_ENDPOINT_URL
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("ha_dev_version_mock", "supervisor_client")
|
||||
async def test_async_schedule_already_scheduled(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test not rescheduled if already scheduled."""
|
||||
aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, status=200)
|
||||
aioclient_mock.post(ANALYTICS_SNAPSHOT_ENDPOINT_URL, status=200, json={})
|
||||
|
||||
analytics = Analytics(hass)
|
||||
with patch(
|
||||
"homeassistant.helpers.storage.Store.async_load",
|
||||
return_value={
|
||||
"onboarded": True,
|
||||
"preferences": {ATTR_BASE: True, ATTR_SNAPSHOTS: True},
|
||||
"uuid": "12345",
|
||||
},
|
||||
):
|
||||
await analytics.load()
|
||||
|
||||
await analytics.async_schedule()
|
||||
await analytics.async_schedule()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 2
|
||||
|
||||
assert any(
|
||||
str(call[1]) == ANALYTICS_ENDPOINT_URL_DEV for call in aioclient_mock.mock_calls
|
||||
)
|
||||
assert any(
|
||||
str(call[1]) == ANALYTICS_SNAPSHOT_ENDPOINT_URL
|
||||
for call in aioclient_mock.mock_calls
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("onboarded"), [True, False])
|
||||
@pytest.mark.usefixtures("ha_dev_version_mock")
|
||||
async def test_async_schedule_cancel_when_disabled(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
onboarded: bool,
|
||||
) -> None:
|
||||
"""Test that scheduled tasks are cancelled when disabled."""
|
||||
analytics = Analytics(hass)
|
||||
with patch(
|
||||
"homeassistant.helpers.storage.Store.async_load",
|
||||
return_value={
|
||||
"onboarded": True,
|
||||
"preferences": {ATTR_BASE: True, ATTR_SNAPSHOTS: True},
|
||||
"uuid": "12345",
|
||||
},
|
||||
):
|
||||
await analytics.load()
|
||||
|
||||
await analytics.async_schedule()
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.storage.Store.async_load",
|
||||
return_value={
|
||||
"onboarded": onboarded,
|
||||
"preferences": {ATTR_BASE: False, ATTR_SNAPSHOTS: False},
|
||||
"uuid": "12345",
|
||||
},
|
||||
):
|
||||
await analytics.load()
|
||||
|
||||
await analytics.async_schedule()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=25))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 0
|
||||
|
||||
@@ -45,7 +45,6 @@ async def test_websocket(
|
||||
{"type": "analytics/preferences", "preferences": {"base": True}}
|
||||
)
|
||||
response = await ws_client.receive_json()
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
assert response["result"]["preferences"]["base"]
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "analytics"})
|
||||
|
||||
@@ -1,4 +1,244 @@
|
||||
# serializer version: 1
|
||||
# name: test_deprecated_sensor_issue[apc-apc_deprecated]
|
||||
IssueRegistryItemSnapshot({
|
||||
'active': True,
|
||||
'breaks_in_ha_version': '2026.6.0',
|
||||
'created': <ANY>,
|
||||
'data': None,
|
||||
'dismissed_version': None,
|
||||
'domain': 'apcupsd',
|
||||
'is_fixable': False,
|
||||
'is_persistent': False,
|
||||
'issue_domain': None,
|
||||
'issue_id': 'apc_deprecated_sensor.myups_status_data',
|
||||
'learn_more_url': None,
|
||||
'severity': <IssueSeverity.WARNING: 'warning'>,
|
||||
'translation_key': 'apc_deprecated',
|
||||
'translation_placeholders': dict({
|
||||
'device_id': '<ANY>',
|
||||
'entity_id': 'sensor.myups_status_data',
|
||||
'entity_name': 'Status data',
|
||||
'items': '''
|
||||
- [APC UPS automation (apc)](/config/automation/edit/apcupsd_auto_apc)
|
||||
- [APC UPS script (apc)](/config/script/edit/apcupsd_script_apc)
|
||||
''',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_deprecated_sensor_issue[apcmodel-available_via_device_info]
|
||||
IssueRegistryItemSnapshot({
|
||||
'active': True,
|
||||
'breaks_in_ha_version': '2026.6.0',
|
||||
'created': <ANY>,
|
||||
'data': None,
|
||||
'dismissed_version': None,
|
||||
'domain': 'apcupsd',
|
||||
'is_fixable': False,
|
||||
'is_persistent': False,
|
||||
'issue_domain': None,
|
||||
'issue_id': 'available_via_device_info_sensor.myups_model',
|
||||
'learn_more_url': None,
|
||||
'severity': <IssueSeverity.WARNING: 'warning'>,
|
||||
'translation_key': 'available_via_device_info',
|
||||
'translation_placeholders': dict({
|
||||
'available_via_device_attr': 'model',
|
||||
'device_id': '<ANY>',
|
||||
'entity_id': 'sensor.myups_model',
|
||||
'entity_name': 'Model',
|
||||
'items': '''
|
||||
- [APC UPS automation (apcmodel)](/config/automation/edit/apcupsd_auto_apcmodel)
|
||||
- [APC UPS script (apcmodel)](/config/script/edit/apcupsd_script_apcmodel)
|
||||
''',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_deprecated_sensor_issue[date-date_deprecated]
|
||||
IssueRegistryItemSnapshot({
|
||||
'active': True,
|
||||
'breaks_in_ha_version': '2026.6.0',
|
||||
'created': <ANY>,
|
||||
'data': None,
|
||||
'dismissed_version': None,
|
||||
'domain': 'apcupsd',
|
||||
'is_fixable': False,
|
||||
'is_persistent': False,
|
||||
'issue_domain': None,
|
||||
'issue_id': 'date_deprecated_sensor.myups_status_date',
|
||||
'learn_more_url': None,
|
||||
'severity': <IssueSeverity.WARNING: 'warning'>,
|
||||
'translation_key': 'date_deprecated',
|
||||
'translation_placeholders': dict({
|
||||
'device_id': '<ANY>',
|
||||
'entity_id': 'sensor.myups_status_date',
|
||||
'entity_name': 'Status date',
|
||||
'items': '''
|
||||
- [APC UPS automation (date)](/config/automation/edit/apcupsd_auto_date)
|
||||
- [APC UPS script (date)](/config/script/edit/apcupsd_script_date)
|
||||
''',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_deprecated_sensor_issue[end apc-date_deprecated]
|
||||
IssueRegistryItemSnapshot({
|
||||
'active': True,
|
||||
'breaks_in_ha_version': '2026.6.0',
|
||||
'created': <ANY>,
|
||||
'data': None,
|
||||
'dismissed_version': None,
|
||||
'domain': 'apcupsd',
|
||||
'is_fixable': False,
|
||||
'is_persistent': False,
|
||||
'issue_domain': None,
|
||||
'issue_id': 'date_deprecated_sensor.myups_date_and_time',
|
||||
'learn_more_url': None,
|
||||
'severity': <IssueSeverity.WARNING: 'warning'>,
|
||||
'translation_key': 'date_deprecated',
|
||||
'translation_placeholders': dict({
|
||||
'device_id': '<ANY>',
|
||||
'entity_id': 'sensor.myups_date_and_time',
|
||||
'entity_name': 'Date and time',
|
||||
'items': '''
|
||||
- [APC UPS automation (end apc)](/config/automation/edit/apcupsd_auto_end_apc)
|
||||
- [APC UPS script (end apc)](/config/script/edit/apcupsd_script_end_apc)
|
||||
''',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_deprecated_sensor_issue[firmware-available_via_device_info]
|
||||
IssueRegistryItemSnapshot({
|
||||
'active': True,
|
||||
'breaks_in_ha_version': '2026.6.0',
|
||||
'created': <ANY>,
|
||||
'data': None,
|
||||
'dismissed_version': None,
|
||||
'domain': 'apcupsd',
|
||||
'is_fixable': False,
|
||||
'is_persistent': False,
|
||||
'issue_domain': None,
|
||||
'issue_id': 'available_via_device_info_sensor.myups_firmware_version',
|
||||
'learn_more_url': None,
|
||||
'severity': <IssueSeverity.WARNING: 'warning'>,
|
||||
'translation_key': 'available_via_device_info',
|
||||
'translation_placeholders': dict({
|
||||
'available_via_device_attr': 'hw_version',
|
||||
'device_id': '<ANY>',
|
||||
'entity_id': 'sensor.myups_firmware_version',
|
||||
'entity_name': 'Firmware version',
|
||||
'items': '''
|
||||
- [APC UPS automation (firmware)](/config/automation/edit/apcupsd_auto_firmware)
|
||||
- [APC UPS script (firmware)](/config/script/edit/apcupsd_script_firmware)
|
||||
''',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_deprecated_sensor_issue[model-available_via_device_info]
|
||||
IssueRegistryItemSnapshot({
|
||||
'active': True,
|
||||
'breaks_in_ha_version': '2026.6.0',
|
||||
'created': <ANY>,
|
||||
'data': None,
|
||||
'dismissed_version': None,
|
||||
'domain': 'apcupsd',
|
||||
'is_fixable': False,
|
||||
'is_persistent': False,
|
||||
'issue_domain': None,
|
||||
'issue_id': 'available_via_device_info_sensor.myups_model_2',
|
||||
'learn_more_url': None,
|
||||
'severity': <IssueSeverity.WARNING: 'warning'>,
|
||||
'translation_key': 'available_via_device_info',
|
||||
'translation_placeholders': dict({
|
||||
'available_via_device_attr': 'model',
|
||||
'device_id': '<ANY>',
|
||||
'entity_id': 'sensor.myups_model_2',
|
||||
'entity_name': 'Model',
|
||||
'items': '''
|
||||
- [APC UPS automation (model)](/config/automation/edit/apcupsd_auto_model)
|
||||
- [APC UPS script (model)](/config/script/edit/apcupsd_script_model)
|
||||
''',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_deprecated_sensor_issue[serialno-available_via_device_info]
|
||||
IssueRegistryItemSnapshot({
|
||||
'active': True,
|
||||
'breaks_in_ha_version': '2026.6.0',
|
||||
'created': <ANY>,
|
||||
'data': None,
|
||||
'dismissed_version': None,
|
||||
'domain': 'apcupsd',
|
||||
'is_fixable': False,
|
||||
'is_persistent': False,
|
||||
'issue_domain': None,
|
||||
'issue_id': 'available_via_device_info_sensor.myups_serial_number',
|
||||
'learn_more_url': None,
|
||||
'severity': <IssueSeverity.WARNING: 'warning'>,
|
||||
'translation_key': 'available_via_device_info',
|
||||
'translation_placeholders': dict({
|
||||
'available_via_device_attr': 'serial_number',
|
||||
'device_id': '<ANY>',
|
||||
'entity_id': 'sensor.myups_serial_number',
|
||||
'entity_name': 'Serial number',
|
||||
'items': '''
|
||||
- [APC UPS automation (serialno)](/config/automation/edit/apcupsd_auto_serialno)
|
||||
- [APC UPS script (serialno)](/config/script/edit/apcupsd_script_serialno)
|
||||
''',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_deprecated_sensor_issue[upsname-available_via_device_info]
|
||||
IssueRegistryItemSnapshot({
|
||||
'active': True,
|
||||
'breaks_in_ha_version': '2026.6.0',
|
||||
'created': <ANY>,
|
||||
'data': None,
|
||||
'dismissed_version': None,
|
||||
'domain': 'apcupsd',
|
||||
'is_fixable': False,
|
||||
'is_persistent': False,
|
||||
'issue_domain': None,
|
||||
'issue_id': 'available_via_device_info_sensor.myups_name',
|
||||
'learn_more_url': None,
|
||||
'severity': <IssueSeverity.WARNING: 'warning'>,
|
||||
'translation_key': 'available_via_device_info',
|
||||
'translation_placeholders': dict({
|
||||
'available_via_device_attr': 'name',
|
||||
'device_id': '<ANY>',
|
||||
'entity_id': 'sensor.myups_name',
|
||||
'entity_name': 'Name',
|
||||
'items': '''
|
||||
- [APC UPS automation (upsname)](/config/automation/edit/apcupsd_auto_upsname)
|
||||
- [APC UPS script (upsname)](/config/script/edit/apcupsd_script_upsname)
|
||||
''',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_deprecated_sensor_issue[version-available_via_device_info]
|
||||
IssueRegistryItemSnapshot({
|
||||
'active': True,
|
||||
'breaks_in_ha_version': '2026.6.0',
|
||||
'created': <ANY>,
|
||||
'data': None,
|
||||
'dismissed_version': None,
|
||||
'domain': 'apcupsd',
|
||||
'is_fixable': False,
|
||||
'is_persistent': False,
|
||||
'issue_domain': None,
|
||||
'issue_id': 'available_via_device_info_sensor.myups_daemon_version',
|
||||
'learn_more_url': None,
|
||||
'severity': <IssueSeverity.WARNING: 'warning'>,
|
||||
'translation_key': 'available_via_device_info',
|
||||
'translation_placeholders': dict({
|
||||
'available_via_device_attr': 'sw_version',
|
||||
'device_id': '<ANY>',
|
||||
'entity_id': 'sensor.myups_daemon_version',
|
||||
'entity_name': 'Daemon version',
|
||||
'items': '''
|
||||
- [APC UPS automation (version)](/config/automation/edit/apcupsd_auto_version)
|
||||
- [APC UPS script (version)](/config/script/edit/apcupsd_script_version)
|
||||
''',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.myups_alarm_delay-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -6,7 +6,8 @@ from unittest.mock import AsyncMock
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.apcupsd.const import DOMAIN
|
||||
from homeassistant.components import automation, script
|
||||
from homeassistant.components.apcupsd.const import DEPRECATED_SENSORS, DOMAIN
|
||||
from homeassistant.components.apcupsd.coordinator import REQUEST_REFRESH_COOLDOWN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
@@ -15,7 +16,11 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.dt import utcnow
|
||||
@@ -161,3 +166,76 @@ async def test_sensor_unknown(
|
||||
await hass.async_block_till_done()
|
||||
# The state should become unknown again.
|
||||
assert hass.states.get(last_self_test_id).state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("entity_key", "issue_key"), DEPRECATED_SENSORS.items())
|
||||
async def test_deprecated_sensor_issue(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_request_status: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_key: str,
|
||||
issue_key: str,
|
||||
) -> None:
|
||||
"""Ensure the issue lists automations and scripts referencing a deprecated sensor."""
|
||||
issue_registry = ir.async_get(hass)
|
||||
unique_id = f"{mock_request_status.return_value['SERIALNO']}_{entity_key}"
|
||||
entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, unique_id)
|
||||
assert entity_id
|
||||
|
||||
# No issue yet.
|
||||
issue_id = f"{issue_key}_{entity_id}"
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
|
||||
# Add automations and scripts referencing the deprecated sensor.
|
||||
entity_slug = slugify(entity_key)
|
||||
automation_object_id = f"apcupsd_auto_{entity_slug}"
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"id": automation_object_id,
|
||||
"alias": f"APC UPS automation ({entity_key})",
|
||||
"trigger": {"platform": "state", "entity_id": entity_id},
|
||||
"action": {
|
||||
"action": "automation.turn_on",
|
||||
"target": {"entity_id": f"automation.{automation_object_id}"},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
script.DOMAIN,
|
||||
{
|
||||
script.DOMAIN: {
|
||||
f"apcupsd_script_{entity_slug}": {
|
||||
"alias": f"APC UPS script ({entity_key})",
|
||||
"sequence": [
|
||||
{
|
||||
"condition": "state",
|
||||
"entity_id": entity_id,
|
||||
"state": "on",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.config_entries.async_reload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
# Redact the device ID in the placeholder for consistency.
|
||||
issue.translation_placeholders["device_id"] = "<ANY>"
|
||||
assert issue == snapshot
|
||||
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Assert the issue is no longer present.
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
@@ -5,9 +5,7 @@ from homeassistant.core import HomeAssistant
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def init_integration(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> MockConfigEntry:
|
||||
async def init_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Set up the Brother integration in Home Assistant."""
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
|
||||
@@ -90,15 +90,6 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_unload_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_unload_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.brother.async_unload_entry", return_value=True
|
||||
) as mock_unload_entry:
|
||||
yield mock_unload_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_brother() -> Generator[AsyncMock]:
|
||||
"""Mock the Brother class."""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Define tests for the Brother Printer config flow."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from brother import SnmpError, UnsupportedModelError
|
||||
import pytest
|
||||
@@ -27,17 +27,7 @@ CONFIG = {
|
||||
SECTION_ADVANCED_SETTINGS: {CONF_PORT: 161, CONF_COMMUNITY: "public"},
|
||||
}
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry", "mock_unload_entry")
|
||||
|
||||
|
||||
async def test_show_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the form is served with no input."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["example.local", "127.0.0.1", "2001:db8::1428:57ab"])
|
||||
@@ -49,9 +39,15 @@ async def test_create_entry(
|
||||
config[CONF_HOST] = host
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=config,
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id=result["flow_id"],
|
||||
user_input=config,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
@@ -60,6 +56,7 @@ async def test_create_entry(
|
||||
assert result["data"][CONF_TYPE] == "laser"
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_PORT] == 161
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_COMMUNITY] == "public"
|
||||
assert result["result"].unique_id == "0123456789"
|
||||
|
||||
|
||||
async def test_invalid_hostname(
|
||||
@@ -97,6 +94,7 @@ async def test_invalid_hostname(
|
||||
assert result["data"][CONF_TYPE] == "laser"
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_PORT] == 161
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_COMMUNITY] == "public"
|
||||
assert result["result"].unique_id == "0123456789"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -142,6 +140,7 @@ async def test_errors(
|
||||
assert result["data"][CONF_TYPE] == "laser"
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_PORT] == 161
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_COMMUNITY] == "public"
|
||||
assert result["result"].unique_id == "0123456789"
|
||||
|
||||
|
||||
async def test_unsupported_model_error(
|
||||
@@ -250,32 +249,31 @@ async def test_zeroconf_device_exists_abort(
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.1"
|
||||
|
||||
|
||||
async def test_zeroconf_no_probe_existing_device(hass: HomeAssistant) -> None:
|
||||
async def test_zeroconf_no_probe_existing_device(
|
||||
hass: HomeAssistant, mock_brother_client: AsyncMock
|
||||
) -> None:
|
||||
"""Test we do not probe the device is the host is already configured."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG)
|
||||
entry.add_to_hass(hass)
|
||||
with (
|
||||
patch("homeassistant.components.brother.Brother.initialize"),
|
||||
patch("homeassistant.components.brother.Brother._get_data") as mock_get_data,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
hostname="example.local.",
|
||||
name="Brother Printer",
|
||||
port=None,
|
||||
properties={},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
hostname="example.local.",
|
||||
name="Brother Printer",
|
||||
port=None,
|
||||
properties={},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert len(mock_get_data.mock_calls) == 0
|
||||
mock_brother_client.async_update.assert_not_called()
|
||||
|
||||
|
||||
async def test_zeroconf_confirm_create_entry(
|
||||
@@ -315,6 +313,7 @@ async def test_zeroconf_confirm_create_entry(
|
||||
assert result["data"][CONF_TYPE] == "laser"
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_PORT] == 161
|
||||
assert result["data"][SECTION_ADVANCED_SETTINGS][CONF_COMMUNITY] == "public"
|
||||
assert result["result"].unique_id == "0123456789"
|
||||
|
||||
|
||||
async def test_reconfigure_successful(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test area_registry API."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
@@ -346,3 +347,92 @@ async def test_update_area_with_name_already_in_use(
|
||||
assert msg["error"]["code"] == "invalid_info"
|
||||
assert msg["error"]["message"] == "The name mock 2 (mock2) is already in use"
|
||||
assert len(area_registry.areas) == 2
|
||||
|
||||
|
||||
async def test_reorder_areas(
|
||||
client: MockHAClientWebSocket, area_registry: ar.AreaRegistry
|
||||
) -> None:
|
||||
"""Test reorder areas."""
|
||||
area1 = area_registry.async_create("mock 1")
|
||||
area2 = area_registry.async_create("mock 2")
|
||||
area3 = area_registry.async_create("mock 3")
|
||||
|
||||
await client.send_json_auto_id({"type": "config/area_registry/list"})
|
||||
msg = await client.receive_json()
|
||||
assert [area["area_id"] for area in msg["result"]] == [area1.id, area2.id, area3.id]
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "config/area_registry/reorder",
|
||||
"area_ids": [area3.id, area1.id, area2.id],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
await client.send_json_auto_id({"type": "config/area_registry/list"})
|
||||
msg = await client.receive_json()
|
||||
assert [area["area_id"] for area in msg["result"]] == [area3.id, area1.id, area2.id]
|
||||
|
||||
|
||||
async def test_reorder_areas_invalid_area_ids(
|
||||
client: MockHAClientWebSocket, area_registry: ar.AreaRegistry
|
||||
) -> None:
|
||||
"""Test reorder with invalid area IDs."""
|
||||
area1 = area_registry.async_create("mock 1")
|
||||
area_registry.async_create("mock 2")
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "config/area_registry/reorder",
|
||||
"area_ids": [area1.id],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "invalid_format"
|
||||
assert "must contain all existing area IDs" in msg["error"]["message"]
|
||||
|
||||
|
||||
async def test_reorder_areas_with_nonexistent_id(
|
||||
client: MockHAClientWebSocket, area_registry: ar.AreaRegistry
|
||||
) -> None:
|
||||
"""Test reorder with nonexistent area ID."""
|
||||
area1 = area_registry.async_create("mock 1")
|
||||
area2 = area_registry.async_create("mock 2")
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "config/area_registry/reorder",
|
||||
"area_ids": [area1.id, area2.id, "nonexistent"],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "invalid_format"
|
||||
|
||||
|
||||
async def test_reorder_areas_persistence(
|
||||
hass: HomeAssistant,
|
||||
client: MockHAClientWebSocket,
|
||||
area_registry: ar.AreaRegistry,
|
||||
hass_storage: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test that area reordering is persisted."""
|
||||
area1 = area_registry.async_create("mock 1")
|
||||
area2 = area_registry.async_create("mock 2")
|
||||
area3 = area_registry.async_create("mock 3")
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "config/area_registry/reorder",
|
||||
"area_ids": [area2.id, area3.id, area1.id],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
area_ids = [area.id for area in area_registry.async_list_areas()]
|
||||
assert area_ids == [area2.id, area3.id, area1.id]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test floor registry API."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
@@ -275,3 +276,100 @@ async def test_update_with_name_already_in_use(
|
||||
== "The name Second floor (secondfloor) is already in use"
|
||||
)
|
||||
assert len(floor_registry.floors) == 2
|
||||
|
||||
|
||||
async def test_reorder_floors(
|
||||
client: MockHAClientWebSocket, floor_registry: fr.FloorRegistry
|
||||
) -> None:
|
||||
"""Test reorder floors."""
|
||||
floor1 = floor_registry.async_create("First floor")
|
||||
floor2 = floor_registry.async_create("Second floor")
|
||||
floor3 = floor_registry.async_create("Third floor")
|
||||
|
||||
await client.send_json_auto_id({"type": "config/floor_registry/list"})
|
||||
msg = await client.receive_json()
|
||||
assert [floor["floor_id"] for floor in msg["result"]] == [
|
||||
floor1.floor_id,
|
||||
floor2.floor_id,
|
||||
floor3.floor_id,
|
||||
]
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "config/floor_registry/reorder",
|
||||
"floor_ids": [floor3.floor_id, floor1.floor_id, floor2.floor_id],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
await client.send_json_auto_id({"type": "config/floor_registry/list"})
|
||||
msg = await client.receive_json()
|
||||
assert [floor["floor_id"] for floor in msg["result"]] == [
|
||||
floor3.floor_id,
|
||||
floor1.floor_id,
|
||||
floor2.floor_id,
|
||||
]
|
||||
|
||||
|
||||
async def test_reorder_floors_invalid_floor_ids(
|
||||
client: MockHAClientWebSocket, floor_registry: fr.FloorRegistry
|
||||
) -> None:
|
||||
"""Test reorder with invalid floor IDs."""
|
||||
floor1 = floor_registry.async_create("First floor")
|
||||
floor_registry.async_create("Second floor")
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "config/floor_registry/reorder",
|
||||
"floor_ids": [floor1.floor_id],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "invalid_format"
|
||||
assert "must contain all existing floor IDs" in msg["error"]["message"]
|
||||
|
||||
|
||||
async def test_reorder_floors_with_nonexistent_id(
|
||||
client: MockHAClientWebSocket, floor_registry: fr.FloorRegistry
|
||||
) -> None:
|
||||
"""Test reorder with nonexistent floor ID."""
|
||||
floor1 = floor_registry.async_create("First floor")
|
||||
floor2 = floor_registry.async_create("Second floor")
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "config/floor_registry/reorder",
|
||||
"floor_ids": [floor1.floor_id, floor2.floor_id, "nonexistent"],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "invalid_format"
|
||||
|
||||
|
||||
async def test_reorder_floors_persistence(
|
||||
hass: HomeAssistant,
|
||||
client: MockHAClientWebSocket,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
hass_storage: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test that floor reordering is persisted."""
|
||||
floor1 = floor_registry.async_create("First floor")
|
||||
floor2 = floor_registry.async_create("Second floor")
|
||||
floor3 = floor_registry.async_create("Third floor")
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "config/floor_registry/reorder",
|
||||
"floor_ids": [floor2.floor_id, floor3.floor_id, floor1.floor_id],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
floor_ids = [floor.floor_id for floor in floor_registry.async_list_floors()]
|
||||
assert floor_ids == [floor2.floor_id, floor3.floor_id, floor1.floor_id]
|
||||
|
||||
@@ -301,3 +301,274 @@ async def test_set_user_data(
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
assert res["result"]["value"] == "test-value"
|
||||
|
||||
|
||||
async def test_get_system_data_empty(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
"""Test get_system_data command."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json(
|
||||
{"id": 5, "type": "frontend/get_system_data", "key": "non-existing-key"}
|
||||
)
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
assert res["result"]["value"] is None
|
||||
|
||||
|
||||
async def test_get_system_data(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
hass_storage: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test get_system_data command."""
|
||||
storage_key = f"{DOMAIN}.system_data"
|
||||
hass_storage[storage_key] = {
|
||||
"key": storage_key,
|
||||
"version": 1,
|
||||
"data": {"test-key": "test-value", "test-complex": [{"foo": "bar"}]},
|
||||
}
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
# Get a simple string key
|
||||
|
||||
await client.send_json(
|
||||
{"id": 6, "type": "frontend/get_system_data", "key": "test-key"}
|
||||
)
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
assert res["result"]["value"] == "test-value"
|
||||
|
||||
# Get a more complex key
|
||||
|
||||
await client.send_json(
|
||||
{"id": 7, "type": "frontend/get_system_data", "key": "test-complex"}
|
||||
)
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
assert res["result"]["value"][0]["foo"] == "bar"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("subscriptions", "events"),
|
||||
[
|
||||
([], []),
|
||||
([(1, {"key": "test-key"}, None)], [(1, "test-value")]),
|
||||
([(1, {"key": "other-key"}, None)], []),
|
||||
],
|
||||
)
|
||||
async def test_set_system_data_empty(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
subscriptions: list[tuple[int, dict[str, str], Any]],
|
||||
events: list[tuple[int, Any]],
|
||||
) -> None:
|
||||
"""Test set_system_data command.
|
||||
|
||||
Also test subscribing.
|
||||
"""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
for msg_id, key, event_data in subscriptions:
|
||||
await client.send_json(
|
||||
{
|
||||
"id": msg_id,
|
||||
"type": "frontend/subscribe_system_data",
|
||||
}
|
||||
| key
|
||||
)
|
||||
|
||||
event = await client.receive_json()
|
||||
assert event == {
|
||||
"id": msg_id,
|
||||
"type": "event",
|
||||
"event": {"value": event_data},
|
||||
}
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
|
||||
# test creating
|
||||
|
||||
await client.send_json(
|
||||
{"id": 6, "type": "frontend/get_system_data", "key": "test-key"}
|
||||
)
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
assert res["result"]["value"] is None
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 7,
|
||||
"type": "frontend/set_system_data",
|
||||
"key": "test-key",
|
||||
"value": "test-value",
|
||||
}
|
||||
)
|
||||
|
||||
for msg_id, event_data in events:
|
||||
event = await client.receive_json()
|
||||
assert event == {"id": msg_id, "type": "event", "event": {"value": event_data}}
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
|
||||
await client.send_json(
|
||||
{"id": 8, "type": "frontend/get_system_data", "key": "test-key"}
|
||||
)
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
assert res["result"]["value"] == "test-value"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("subscriptions", "events"),
|
||||
[
|
||||
(
|
||||
[],
|
||||
[[], []],
|
||||
),
|
||||
(
|
||||
[(1, {"key": "test-key"}, "test-value")],
|
||||
[[], []],
|
||||
),
|
||||
(
|
||||
[(1, {"key": "test-non-existent-key"}, None)],
|
||||
[[(1, "test-value-new")], []],
|
||||
),
|
||||
(
|
||||
[(1, {"key": "test-complex"}, "string")],
|
||||
[[], [(1, [{"foo": "bar"}])]],
|
||||
),
|
||||
(
|
||||
[(1, {"key": "other-key"}, None)],
|
||||
[[], []],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_set_system_data(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
hass_storage: dict[str, Any],
|
||||
subscriptions: list[tuple[int, dict[str, str], Any]],
|
||||
events: list[list[tuple[int, Any]]],
|
||||
) -> None:
|
||||
"""Test set_system_data command with initial data."""
|
||||
storage_key = f"{DOMAIN}.system_data"
|
||||
hass_storage[storage_key] = {
|
||||
"version": 1,
|
||||
"data": {"test-key": "test-value", "test-complex": "string"},
|
||||
}
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
for msg_id, key, event_data in subscriptions:
|
||||
await client.send_json(
|
||||
{
|
||||
"id": msg_id,
|
||||
"type": "frontend/subscribe_system_data",
|
||||
}
|
||||
| key
|
||||
)
|
||||
|
||||
event = await client.receive_json()
|
||||
assert event == {
|
||||
"id": msg_id,
|
||||
"type": "event",
|
||||
"event": {"value": event_data},
|
||||
}
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
|
||||
# test creating
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 5,
|
||||
"type": "frontend/set_system_data",
|
||||
"key": "test-non-existent-key",
|
||||
"value": "test-value-new",
|
||||
}
|
||||
)
|
||||
|
||||
for msg_id, event_data in events[0]:
|
||||
event = await client.receive_json()
|
||||
assert event == {"id": msg_id, "type": "event", "event": {"value": event_data}}
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
|
||||
await client.send_json(
|
||||
{"id": 6, "type": "frontend/get_system_data", "key": "test-non-existent-key"}
|
||||
)
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
assert res["result"]["value"] == "test-value-new"
|
||||
|
||||
# test updating with complex data
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 7,
|
||||
"type": "frontend/set_system_data",
|
||||
"key": "test-complex",
|
||||
"value": [{"foo": "bar"}],
|
||||
}
|
||||
)
|
||||
|
||||
for msg_id, event_data in events[1]:
|
||||
event = await client.receive_json()
|
||||
assert event == {"id": msg_id, "type": "event", "event": {"value": event_data}}
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
|
||||
await client.send_json(
|
||||
{"id": 8, "type": "frontend/get_system_data", "key": "test-complex"}
|
||||
)
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
assert res["result"]["value"][0]["foo"] == "bar"
|
||||
|
||||
# ensure other existing key was not modified
|
||||
|
||||
await client.send_json(
|
||||
{"id": 9, "type": "frontend/get_system_data", "key": "test-key"}
|
||||
)
|
||||
|
||||
res = await client.receive_json()
|
||||
assert res["success"], res
|
||||
assert res["result"]["value"] == "test-value"
|
||||
|
||||
|
||||
async def test_set_system_data_requires_admin(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
hass_read_only_access_token: str,
|
||||
) -> None:
|
||||
"""Test set_system_data requires admin permissions."""
|
||||
client = await hass_ws_client(hass, hass_read_only_access_token)
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 5,
|
||||
"type": "frontend/set_system_data",
|
||||
"key": "test-key",
|
||||
"value": "test-value",
|
||||
}
|
||||
)
|
||||
|
||||
res = await client.receive_json()
|
||||
assert not res["success"], res
|
||||
assert res["error"]["code"] == "unauthorized"
|
||||
assert res["error"]["message"] == "Unauthorized"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Lamarzocco session fixtures."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
||||
|
||||
from bleak.backends.device import BLEDevice
|
||||
from pylamarzocco.const import ModelName
|
||||
@@ -132,6 +132,10 @@ def mock_lamarzocco(device_fixture: ModelName) -> Generator[MagicMock]:
|
||||
"schedule": machine_mock.schedule.to_dict(),
|
||||
"settings": machine_mock.settings.to_dict(),
|
||||
}
|
||||
machine_mock.connect_dashboard_websocket = AsyncMock()
|
||||
machine_mock.websocket = MagicMock()
|
||||
machine_mock.websocket.connected = True
|
||||
machine_mock.websocket.disconnect = AsyncMock()
|
||||
yield machine_mock
|
||||
|
||||
|
||||
@@ -149,10 +153,11 @@ def mock_ble_device() -> BLEDevice:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_websocket_terminated() -> Generator[bool]:
|
||||
def mock_websocket_terminated() -> Generator[PropertyMock]:
|
||||
"""Mock websocket terminated."""
|
||||
with patch(
|
||||
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoUpdateCoordinator.websocket_terminated",
|
||||
new=False,
|
||||
new_callable=PropertyMock,
|
||||
) as mock_websocket_terminated:
|
||||
mock_websocket_terminated.return_value = False
|
||||
yield mock_websocket_terminated
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Tests for La Marzocco binary sensors."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pylamarzocco.exceptions import RequestNotSuccessful
|
||||
@@ -36,24 +35,15 @@ async def test_binary_sensors(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_websocket_terminated() -> Generator[bool]:
|
||||
"""Mock websocket terminated."""
|
||||
with patch(
|
||||
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoUpdateCoordinator.websocket_terminated",
|
||||
new=False,
|
||||
) as mock_websocket_terminated:
|
||||
yield mock_websocket_terminated
|
||||
|
||||
|
||||
async def test_brew_active_unavailable(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_websocket_terminated: PropertyMock,
|
||||
) -> None:
|
||||
"""Test the La Marzocco brew active becomes unavailable."""
|
||||
|
||||
mock_lamarzocco.websocket.connected = False
|
||||
mock_websocket_terminated.return_value = True
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
state = hass.states.get(
|
||||
f"binary_sensor.{mock_lamarzocco.serial_number}_brewing_active"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Test initialization of lamarzocco."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pylamarzocco.const import FirmwareType, ModelName
|
||||
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
|
||||
from pylamarzocco.models import WebSocketDetails
|
||||
@@ -30,7 +32,7 @@ from . import (
|
||||
get_bluetooth_service_info,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_load_unload_config_entry(
|
||||
@@ -310,3 +312,37 @@ async def test_device(
|
||||
device = device_registry.async_get(entry.device_id)
|
||||
assert device
|
||||
assert device == snapshot
|
||||
|
||||
|
||||
async def test_websocket_reconnects_after_termination(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_lamarzocco: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test the websocket reconnects after background task terminates."""
|
||||
# Setup: websocket connected initially
|
||||
mock_websocket = MagicMock()
|
||||
mock_websocket.closed = False
|
||||
mock_lamarzocco.websocket = WebSocketDetails(mock_websocket, None)
|
||||
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
|
||||
# Verify initial websocket connection was attempted
|
||||
assert mock_lamarzocco.connect_dashboard_websocket.call_count == 1
|
||||
|
||||
# Simulate websocket disconnection (e.g., after internet outage)
|
||||
mock_websocket.closed = True
|
||||
|
||||
# Simulate the background task terminating by patching websocket_terminated
|
||||
with patch(
|
||||
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoConfigUpdateCoordinator.websocket_terminated",
|
||||
new=True,
|
||||
):
|
||||
# Trigger the coordinator's update (which runs every 60 seconds)
|
||||
freezer.tick(timedelta(seconds=61))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify websocket reconnection was attempted
|
||||
assert mock_lamarzocco.connect_dashboard_websocket.call_count == 2
|
||||
|
||||
@@ -112,7 +112,7 @@ async def integration_fixture(
|
||||
"leak_sensor",
|
||||
"light_sensor",
|
||||
"microwave_oven",
|
||||
"mock_thermostat",
|
||||
"mock_lock",
|
||||
"mounted_dimmable_load_control_fixture",
|
||||
"multi_endpoint_light",
|
||||
"occupancy_sensor",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"node_id": 150,
|
||||
"date_commissioned": "2025-11-18T06:53:08.679289",
|
||||
"last_interview": "2025-11-18T06:53:08.679325",
|
||||
"node_id": 151,
|
||||
"date_commissioned": "2025-11-18T20:53:49.656346",
|
||||
"last_interview": "2025-11-18T20:53:49.656374",
|
||||
"interview_version": 6,
|
||||
"available": true,
|
||||
"is_bridge": false,
|
||||
@@ -22,6 +22,14 @@
|
||||
"0/49/65528": [],
|
||||
"0/49/65529": [],
|
||||
"0/49/65531": [0, 1, 4, 5, 6, 7, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/70/0": 300,
|
||||
"0/70/1": 300,
|
||||
"0/70/2": 5000,
|
||||
"0/70/65532": 0,
|
||||
"0/70/65533": 3,
|
||||
"0/70/65528": [],
|
||||
"0/70/65529": [],
|
||||
"0/70/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/65/0": [],
|
||||
"0/65/65532": 0,
|
||||
"0/65/65533": 1,
|
||||
@@ -39,7 +47,7 @@
|
||||
"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==",
|
||||
"1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRlxgkBwEkCAEwCUEEXezptKw8pXZnNDtNP/6ytcTdilcvU5tGmhRizeaObdQklNW6TLSBvoX5icYne5L5ESsO6yuHCKieC120pOsVVjcKNQEoARgkAgE2AwQCBAEYMAQUtiskyCanVk/hj/Gf0pOAxSBXGLAwBRS5+zzv8ZPGnI9mC3wH9vq10JnwlhgwC0DrKZjxUW15mceAFXSKE0BCJf3c4iBeFXaK9nrYnHOfiOH9QYqZ71iUyvOrlahFR9xELuLKaj5j/NlVR3CVGU/KGA==",
|
||||
"2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE/DujEcdTsX19xbxX+KuKKWiMaA5D9u99P/pVxIOmscd2BA2PadEMNnjvtPOpf+WE2Zxar4rby1IfAClGUUuQrTcKNQEpARgkAmAwBBS5+zzv8ZPGnI9mC3wH9vq10JnwljAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQGkPpvsbkAFEbfPN6H3Kf23R0zzmW/gpAA3kgaL6wKB2Ofm+Tmylw22qM536Kj8mOMwaV0EL1dCCGcuxF98aL6gY",
|
||||
"254": 1
|
||||
}
|
||||
@@ -49,7 +57,7 @@
|
||||
"1": "BBmX+KwLR5HGlVNbvlC+dO8Jv9fPthHiTfGpUzi2JJADX5az6GxBAFn02QKHwLcZHyh+lh9faf6rf38/nPYF7/M=",
|
||||
"2": 4939,
|
||||
"3": 2,
|
||||
"4": 150,
|
||||
"4": 151,
|
||||
"5": "ha",
|
||||
"254": 1
|
||||
}
|
||||
@@ -73,15 +81,15 @@
|
||||
"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/2": 894,
|
||||
"0/55/3": 64,
|
||||
"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/8": 19,
|
||||
"0/55/65532": 3,
|
||||
"0/55/65533": 1,
|
||||
"0/55/65528": [],
|
||||
@@ -112,29 +120,29 @@
|
||||
],
|
||||
"0/52/0": [
|
||||
{
|
||||
"0": 6163,
|
||||
"1": "6163"
|
||||
"0": 12306,
|
||||
"1": "12306"
|
||||
},
|
||||
{
|
||||
"0": 6162,
|
||||
"1": "6162"
|
||||
"0": 12305,
|
||||
"1": "12305"
|
||||
},
|
||||
{
|
||||
"0": 6161,
|
||||
"1": "6161"
|
||||
"0": 12304,
|
||||
"1": "12304"
|
||||
},
|
||||
{
|
||||
"0": 6160,
|
||||
"1": "6160"
|
||||
"0": 12303,
|
||||
"1": "12303"
|
||||
},
|
||||
{
|
||||
"0": 6159,
|
||||
"1": "6159"
|
||||
"0": 12302,
|
||||
"1": "12302"
|
||||
}
|
||||
],
|
||||
"0/52/1": 545392,
|
||||
"0/52/2": 650640,
|
||||
"0/52/3": 650640,
|
||||
"0/52/1": 537280,
|
||||
"0/52/2": 662848,
|
||||
"0/52/3": 662848,
|
||||
"0/52/65532": 1,
|
||||
"0/52/65533": 1,
|
||||
"0/52/65528": [],
|
||||
@@ -159,7 +167,7 @@
|
||||
"4": "AAwpaqXN",
|
||||
"5": ["wKgBxA=="],
|
||||
"6": [
|
||||
"KgEOCgKzOZAcmuLd4EsaUA==",
|
||||
"KgEOCgKzOZBI/+PPpfBv9Q==",
|
||||
"KgEOCgKzOZA2wMm9YG06Ag==",
|
||||
"/oAAAAAAAACluAo+qvkuxw=="
|
||||
],
|
||||
@@ -180,7 +188,7 @@
|
||||
"0/51/8": false,
|
||||
"0/51/3": 0,
|
||||
"0/51/4": 0,
|
||||
"0/51/2": 16,
|
||||
"0/51/2": 19,
|
||||
"0/51/65532": 0,
|
||||
"0/51/65533": 2,
|
||||
"0/51/65528": [2],
|
||||
@@ -204,17 +212,10 @@
|
||||
"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/3": "Mock Lock",
|
||||
"0/40/4": 32769,
|
||||
"0/40/5": "",
|
||||
"0/40/6": "**REDACTED**",
|
||||
@@ -235,7 +236,7 @@
|
||||
"0/40/14": "",
|
||||
"0/40/15": "TEST_SN",
|
||||
"0/40/16": false,
|
||||
"0/40/18": "29DB8B9DB518F05F",
|
||||
"0/40/18": "714373B6E9864224",
|
||||
"0/40/65532": 0,
|
||||
"0/40/65533": 5,
|
||||
"0/40/65528": [],
|
||||
@@ -256,30 +257,24 @@
|
||||
"0/31/2": 4,
|
||||
"0/31/3": 3,
|
||||
"0/31/4": 4,
|
||||
"0/31/65532": 0,
|
||||
"0/31/1": [],
|
||||
"0/31/65532": 1,
|
||||
"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/31/65531": [0, 2, 3, 4, 1, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/29/0": [
|
||||
{
|
||||
"0": 18,
|
||||
"0": 17,
|
||||
"1": 1
|
||||
},
|
||||
{
|
||||
"0": 22,
|
||||
"1": 3
|
||||
"1": 4
|
||||
}
|
||||
],
|
||||
"0/29/1": [
|
||||
49, 65, 63, 62, 60, 55, 54, 52, 51, 50, 48, 43, 40, 31, 30, 29, 3, 42, 45,
|
||||
53
|
||||
49, 70, 65, 63, 62, 60, 55, 54, 52, 51, 50, 48, 40, 31, 29, 42, 47, 53
|
||||
],
|
||||
"0/29/2": [41],
|
||||
"0/29/3": [1],
|
||||
@@ -288,13 +283,6 @@
|
||||
"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,
|
||||
@@ -304,12 +292,17 @@
|
||||
"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/47/0": 1,
|
||||
"0/47/1": 0,
|
||||
"0/47/2": "USB",
|
||||
"0/47/5": 0,
|
||||
"0/47/6": 0,
|
||||
"0/47/31": [],
|
||||
"0/47/65532": 1,
|
||||
"0/47/65533": 3,
|
||||
"0/47/65528": [],
|
||||
"0/47/65529": [],
|
||||
"0/47/65531": [0, 1, 2, 5, 6, 31, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/53/0": null,
|
||||
"0/53/1": null,
|
||||
"0/53/2": null,
|
||||
@@ -383,144 +376,96 @@
|
||||
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/3/0": 0,
|
||||
"1/3/1": 2,
|
||||
"1/3/65532": 0,
|
||||
"1/3/65533": 6,
|
||||
"1/3/65528": [],
|
||||
"1/3/65529": [0],
|
||||
"1/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531],
|
||||
"1/29/0": [
|
||||
{
|
||||
"0": 769,
|
||||
"1": 4
|
||||
"0": 10,
|
||||
"1": 3
|
||||
},
|
||||
{
|
||||
"0": 17,
|
||||
"1": 1
|
||||
}
|
||||
],
|
||||
"1/29/1": [29, 3, 4, 513, 516],
|
||||
"1/29/2": [3],
|
||||
"1/29/1": [3, 29, 47, 257],
|
||||
"1/29/2": [],
|
||||
"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": 1761951600,
|
||||
"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/47/0": 1,
|
||||
"1/47/1": 1,
|
||||
"1/47/2": "Battery",
|
||||
"1/47/14": 0,
|
||||
"1/47/15": false,
|
||||
"1/47/16": 0,
|
||||
"1/47/19": "",
|
||||
"1/47/25": 1,
|
||||
"1/47/31": [],
|
||||
"1/47/65532": 10,
|
||||
"1/47/65533": 3,
|
||||
"1/47/65528": [],
|
||||
"1/47/65529": [],
|
||||
"1/47/65531": [
|
||||
0, 1, 2, 14, 15, 16, 19, 25, 31, 65532, 65533, 65528, 65529, 65531
|
||||
],
|
||||
"1/513/73": [
|
||||
{
|
||||
"0": 4,
|
||||
"1": 1,
|
||||
"2": 2
|
||||
},
|
||||
{
|
||||
"0": 3,
|
||||
"1": 1,
|
||||
"2": 2
|
||||
}
|
||||
"1/257/0": 1,
|
||||
"1/257/1": 0,
|
||||
"1/257/2": true,
|
||||
"1/257/3": 1,
|
||||
"1/257/4": 10,
|
||||
"1/257/5": 10,
|
||||
"1/257/17": 10,
|
||||
"1/257/18": 10,
|
||||
"1/257/19": 10,
|
||||
"1/257/20": 10,
|
||||
"1/257/21": 10,
|
||||
"1/257/22": 10,
|
||||
"1/257/23": 8,
|
||||
"1/257/24": 6,
|
||||
"1/257/25": 20,
|
||||
"1/257/26": 10,
|
||||
"1/257/27": 1,
|
||||
"1/257/28": 5,
|
||||
"1/257/33": "en",
|
||||
"1/257/35": 60,
|
||||
"1/257/36": 0,
|
||||
"1/257/37": 0,
|
||||
"1/257/38": 65526,
|
||||
"1/257/41": false,
|
||||
"1/257/43": false,
|
||||
"1/257/48": 3,
|
||||
"1/257/49": 10,
|
||||
"1/257/51": false,
|
||||
"1/257/128": null,
|
||||
"1/257/129": null,
|
||||
"1/257/130": "JE4bj1uR9Kt/VH7lYJwxbw==",
|
||||
"1/257/131": ["AQA="],
|
||||
"1/257/132": null,
|
||||
"1/257/133": ["AQA="],
|
||||
"1/257/134": 0,
|
||||
"1/257/135": 10,
|
||||
"1/257/136": 10,
|
||||
"1/257/65532": 32179,
|
||||
"1/257/65533": 8,
|
||||
"1/257/65528": [12, 15, 18, 28, 35, 37],
|
||||
"1/257/65529": [
|
||||
0, 1, 3, 11, 12, 13, 14, 15, 16, 17, 18, 19, 26, 27, 29, 34, 36, 38, 39,
|
||||
40, 41
|
||||
],
|
||||
"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": 419,
|
||||
"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]
|
||||
"1/257/65531": [
|
||||
0, 1, 2, 3, 4, 5, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 33, 35,
|
||||
36, 37, 38, 41, 43, 48, 49, 51, 128, 129, 130, 131, 132, 133, 134, 135,
|
||||
136, 65532, 65533, 65528, 65529, 65531
|
||||
]
|
||||
},
|
||||
"attribute_subscriptions": []
|
||||
}
|
||||
@@ -489,6 +489,104 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[mock_lock][binary_sensor.mock_lock_battery-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_lock_battery',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.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-0000000000000097-MatterNodeDevice-1-BatteryChargeLevel-47-14',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[mock_lock][binary_sensor.mock_lock_battery-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'Mock Lock Battery',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.mock_lock_battery',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[mock_lock][binary_sensor.mock_lock_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.mock_lock_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Door',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-0000000000000097-MatterNodeDevice-1-LockDoorStateSensor-257-3',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[mock_lock][binary_sensor.mock_lock_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'Mock Lock Door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.mock_lock_door',
|
||||
'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({
|
||||
|
||||
@@ -2241,7 +2241,7 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_buttons[mock_thermostat][button.mock_thermostat_identify_0-entry]
|
||||
# name: test_buttons[mock_lock][button.mock_lock_identify-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -2254,7 +2254,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'button.mock_thermostat_identify_0',
|
||||
'entity_id': 'button.mock_lock_identify',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -2266,73 +2266,24 @@
|
||||
}),
|
||||
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Identify (0)',
|
||||
'original_name': 'Identify',
|
||||
'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',
|
||||
'unique_id': '00000000000004D2-0000000000000097-MatterNodeDevice-1-IdentifyButton-3-1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_buttons[mock_thermostat][button.mock_thermostat_identify_0-state]
|
||||
# name: test_buttons[mock_lock][button.mock_lock_identify-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'identify',
|
||||
'friendly_name': 'Mock Thermostat Identify (0)',
|
||||
'friendly_name': 'Mock Lock Identify',
|
||||
}),
|
||||
'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.CONFIG: 'config'>,
|
||||
'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',
|
||||
'entity_id': 'button.mock_lock_identify',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
||||
@@ -325,77 +325,6 @@
|
||||
'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({
|
||||
|
||||
@@ -149,3 +149,53 @@
|
||||
'state': 'locked',
|
||||
})
|
||||
# ---
|
||||
# name: test_locks[mock_lock][lock.mock_lock-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': 'lock',
|
||||
'entity_category': None,
|
||||
'entity_id': 'lock.mock_lock',
|
||||
'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': <LockEntityFeature: 1>,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-0000000000000097-MatterNodeDevice-1-MatterLock-257-0',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_locks[mock_lock][lock.mock_lock-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'changed_by': 'Unknown',
|
||||
'friendly_name': 'Mock Lock',
|
||||
'supported_features': <LockEntityFeature: 1>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'lock.mock_lock',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'locked',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -1731,6 +1731,179 @@
|
||||
'state': '30',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[mock_lock][number.mock_lock_auto_relock_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 65534,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.mock_lock_auto_relock_time',
|
||||
'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': 'Auto-relock time',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'auto_relock_timer',
|
||||
'unique_id': '00000000000004D2-0000000000000097-MatterNodeDevice-1-AutoRelockTimer-257-35',
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[mock_lock][number.mock_lock_auto_relock_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Lock Auto-relock time',
|
||||
'max': 65534,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.mock_lock_auto_relock_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '60',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[mock_lock][number.mock_lock_user_code_temporary_disable_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 255,
|
||||
'min': 1,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.mock_lock_user_code_temporary_disable_time',
|
||||
'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': 'User code temporary disable time',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'user_code_temporary_disable_time',
|
||||
'unique_id': '00000000000004D2-0000000000000097-MatterNodeDevice-1-DoorLockUserCodeTemporaryDisableTime-257-49',
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[mock_lock][number.mock_lock_user_code_temporary_disable_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Lock User code temporary disable time',
|
||||
'max': 255,
|
||||
'min': 1,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1,
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.mock_lock_user_code_temporary_disable_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '10',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[mock_lock][number.mock_lock_wrong_code_limit-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 255,
|
||||
'min': 1,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.mock_lock_wrong_code_limit',
|
||||
'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': 'Wrong code limit',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'wrong_code_entry_limit',
|
||||
'unique_id': '00000000000004D2-0000000000000097-MatterNodeDevice-1-DoorLockWrongCodeEntryLimit-257-48',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[mock_lock][number.mock_lock_wrong_code_limit-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Lock Wrong code limit',
|
||||
'max': 255,
|
||||
'min': 1,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.mock_lock_wrong_code_limit',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '3',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[mounted_dimmable_load_control_fixture][number.mock_mounted_dimmable_load_control_on_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -2257,15 +2257,17 @@
|
||||
'state': '1000',
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[mock_thermostat][select.mock_thermostat_temperature_display_mode-entry]
|
||||
# name: test_selects[mock_lock][select.mock_lock_sound_volume-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'Celsius',
|
||||
'Fahrenheit',
|
||||
'silent',
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
@@ -2275,7 +2277,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.mock_thermostat_temperature_display_mode',
|
||||
'entity_id': 'select.mock_lock_sound_volume',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -2287,31 +2289,33 @@
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Temperature display mode',
|
||||
'original_name': 'Sound volume',
|
||||
'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',
|
||||
'translation_key': 'door_lock_sound_volume',
|
||||
'unique_id': '00000000000004D2-0000000000000097-MatterNodeDevice-1-DoorLockSoundVolume-257-36',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[mock_thermostat][select.mock_thermostat_temperature_display_mode-state]
|
||||
# name: test_selects[mock_lock][select.mock_lock_sound_volume-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Thermostat Temperature display mode',
|
||||
'friendly_name': 'Mock Lock Sound volume',
|
||||
'options': list([
|
||||
'Celsius',
|
||||
'Fahrenheit',
|
||||
'silent',
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.mock_thermostat_temperature_display_mode',
|
||||
'entity_id': 'select.mock_lock_sound_volume',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Celsius',
|
||||
'state': 'silent',
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[mounted_dimmable_load_control_fixture][select.mock_mounted_dimmable_load_control_power_on_behavior_on_startup-entry]
|
||||
|
||||
@@ -7259,332 +7259,6 @@
|
||||
'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_last_change-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': None,
|
||||
'entity_id': 'sensor.mock_thermostat_last_change',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Last change',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'setpoint_change_timestamp',
|
||||
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-SetpointChangeSourceTimestamp-513-50',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'Mock Thermostat Last change',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_thermostat_last_change',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2025-10-31T23:00:00+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_amount-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_last_change_amount',
|
||||
'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': 'Last change amount',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'setpoint_change_amount',
|
||||
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatSetpointChangeAmount-513-49',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_amount-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Mock Thermostat Last change amount',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_thermostat_last_change_amount',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_source-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'manual',
|
||||
'schedule',
|
||||
'external',
|
||||
]),
|
||||
}),
|
||||
'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_last_change_source',
|
||||
'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': 'Last change source',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'setpoint_change_source',
|
||||
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-SetpointChangeSource-513-48',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_source-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Mock Thermostat Last change source',
|
||||
'options': list([
|
||||
'manual',
|
||||
'schedule',
|
||||
'external',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_thermostat_last_change_source',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'manual',
|
||||
})
|
||||
# ---
|
||||
# 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({
|
||||
|
||||
@@ -682,6 +682,54 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[mock_lock][switch.mock_lock_privacy_mode_button-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.mock_lock_privacy_mode_button',
|
||||
'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': 'Privacy mode button',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'privacy_mode_button',
|
||||
'unique_id': '00000000000004D2-0000000000000097-MatterNodeDevice-1-DoorLockEnablePrivacyModeButton-257-43',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[mock_lock][switch.mock_lock_privacy_mode_button-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Lock Privacy mode button',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.mock_lock_privacy_mode_button',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[on_off_plugin_unit][switch.mock_onoffpluginunit-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -697,91 +697,3 @@ async def test_vacuum_operational_error_sensor(
|
||||
state = hass.states.get("sensor.mock_vacuum_operational_error")
|
||||
assert state
|
||||
assert state.state == "unknown"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_thermostat"])
|
||||
async def test_thermostat_setpoint_change_source(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test Thermostat SetpointChangeSource sensor."""
|
||||
# Thermostat Cluster / SetpointChangeSource attribute (1/513/48)
|
||||
state = hass.states.get("sensor.mock_thermostat_last_change_source")
|
||||
assert state
|
||||
assert state.state == "manual"
|
||||
assert state.attributes["options"] == ["manual", "schedule", "external"]
|
||||
|
||||
# Test schedule source
|
||||
set_node_attribute(matter_node, 1, 513, 48, 1)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("sensor.mock_thermostat_last_change_source")
|
||||
assert state
|
||||
assert state.state == "schedule"
|
||||
|
||||
# Test external source
|
||||
set_node_attribute(matter_node, 1, 513, 48, 2)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("sensor.mock_thermostat_last_change_source")
|
||||
assert state
|
||||
assert state.state == "external"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_thermostat"])
|
||||
async def test_thermostat_setpoint_change_timestamp(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test Thermostat SetpointChangeSourceTimestamp sensor."""
|
||||
# Thermostat Cluster / SetpointChangeSourceTimestamp attribute (1/513/50)
|
||||
state = hass.states.get("sensor.mock_thermostat_last_change")
|
||||
assert state
|
||||
assert state.state == "2025-10-31T23:00:00+00:00"
|
||||
|
||||
# Update to a new timestamp (2024-11-15 12:00:00 UTC)
|
||||
set_node_attribute(matter_node, 1, 513, 50, 1731672000)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("sensor.mock_thermostat_last_change")
|
||||
assert state
|
||||
assert state.state == "2024-11-15T12:00:00+00:00"
|
||||
|
||||
# Test zero value (should be None/unknown)
|
||||
set_node_attribute(matter_node, 1, 513, 50, 0)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("sensor.mock_thermostat_last_change")
|
||||
assert state
|
||||
assert state.state == "unknown"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_thermostat"])
|
||||
async def test_thermostat_setpoint_change_amount(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test Thermostat SetpointChangeAmount sensor."""
|
||||
# Thermostat Cluster / SetpointChangeAmount attribute (1/513/49)
|
||||
state = hass.states.get("sensor.mock_thermostat_last_change_amount")
|
||||
assert state
|
||||
assert state.state == "1.5"
|
||||
|
||||
# Update to 2.0°C (200 in Matter units)
|
||||
set_node_attribute(matter_node, 1, 513, 49, 200)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("sensor.mock_thermostat_last_change_amount")
|
||||
assert state
|
||||
assert state.state == "2.0"
|
||||
|
||||
# Update to -0.5°C (-50 in Matter units)
|
||||
set_node_attribute(matter_node, 1, 513, 49, -50)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("sensor.mock_thermostat_last_change_amount")
|
||||
assert state
|
||||
assert state.state == "-0.5"
|
||||
|
||||
@@ -52,7 +52,8 @@ def mock_client():
|
||||
client = client.return_value
|
||||
client.async_login.return_value = True
|
||||
client.status = ChargerStatus.CHARGING
|
||||
client.power = ChargerPower(0, 0, 0, 0)
|
||||
client.power = ChargerPower(0, 0, 0)
|
||||
client.available = True
|
||||
|
||||
client.target_soc = 50
|
||||
client.target_time = (8, 0)
|
||||
|
||||
@@ -47,59 +47,6 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.ohme_home_pro_ct_current-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': None,
|
||||
'entity_id': 'sensor.ohme_home_pro_ct_current',
|
||||
'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.CURRENT: 'current'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'CT current',
|
||||
'platform': 'ohme',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'ct_current',
|
||||
'unique_id': 'chargerid_ct_current',
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.ohme_home_pro_ct_current-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'current',
|
||||
'friendly_name': 'Ohme Home Pro CT current',
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.ohme_home_pro_ct_current',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.ohme_home_pro_current-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
dict({
|
||||
'end': '2024-12-30T04:30:39+00:00',
|
||||
'energy': 2.042,
|
||||
'power': 4.0,
|
||||
'start': '2024-12-30T04:00:00+00:00',
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -1 +1,18 @@
|
||||
"""Tests for the Sunricher Sunricher DALI integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from PySrDaliGateway import CallbackEventType
|
||||
|
||||
|
||||
def find_device_listener(
|
||||
device: MagicMock, event_type: CallbackEventType
|
||||
) -> Callable[..., None]:
|
||||
"""Find the registered listener callback for a specific device and event type."""
|
||||
for call in device.register_listener.call_args_list:
|
||||
if call[0][0] == event_type:
|
||||
return call[0][1]
|
||||
raise AssertionError(
|
||||
f"Listener for event type {event_type} not found on device {device.dev_id}"
|
||||
)
|
||||
|
||||
@@ -56,6 +56,7 @@ def _create_mock_device(
|
||||
device.turn_on = MagicMock()
|
||||
device.turn_off = MagicMock()
|
||||
device.read_status = MagicMock()
|
||||
device.register_listener = MagicMock(return_value=lambda: None)
|
||||
return device
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PySrDaliGateway import CallbackEventType
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.light import (
|
||||
@@ -15,6 +16,8 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import find_device_listener
|
||||
|
||||
from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform
|
||||
|
||||
TEST_DIMMER_ENTITY_ID = "light.dimmer_0000_02"
|
||||
@@ -24,13 +27,20 @@ TEST_HS_DEVICE_ID = "01030000046A242121110E"
|
||||
TEST_RGBW_DEVICE_ID = "01040000056A242121110E"
|
||||
|
||||
|
||||
def _dispatch_status(
|
||||
gateway: MagicMock, device_id: str, status: dict[str, Any]
|
||||
def _trigger_light_status_callback(
|
||||
device: MagicMock, device_id: str, status: dict[str, Any]
|
||||
) -> None:
|
||||
"""Invoke the status callback registered on the gateway mock."""
|
||||
callback = gateway.on_light_status
|
||||
assert callable(callback)
|
||||
callback(device_id, status)
|
||||
"""Trigger the light status callbacks registered on the device mock."""
|
||||
callback = find_device_listener(device, CallbackEventType.LIGHT_STATUS)
|
||||
callback(status)
|
||||
|
||||
|
||||
def _trigger_availability_callback(
|
||||
device: MagicMock, device_id: str, available: bool
|
||||
) -> None:
|
||||
"""Trigger the availability callbacks registered on the device mock."""
|
||||
callback = find_device_listener(device, CallbackEventType.ONLINE_STATUS)
|
||||
callback(available)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -133,26 +143,25 @@ async def test_turn_on_with_brightness(
|
||||
)
|
||||
|
||||
|
||||
async def test_dispatcher_connection(
|
||||
async def test_callback_registration(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_devices: list[MagicMock],
|
||||
mock_gateway: MagicMock,
|
||||
) -> None:
|
||||
"""Test that dispatcher signals are properly connected."""
|
||||
entity_entry = er.async_get(hass).async_get(TEST_DIMMER_ENTITY_ID)
|
||||
assert entity_entry is not None
|
||||
|
||||
state = hass.states.get(TEST_DIMMER_ENTITY_ID)
|
||||
assert state is not None
|
||||
"""Test that callbacks are properly registered and triggered."""
|
||||
state_before = hass.states.get(TEST_DIMMER_ENTITY_ID)
|
||||
assert state_before is not None
|
||||
|
||||
status_update: dict[str, Any] = {"is_on": True, "brightness": 128}
|
||||
|
||||
_dispatch_status(mock_gateway, TEST_DIMMER_DEVICE_ID, status_update)
|
||||
_trigger_light_status_callback(
|
||||
mock_devices[0], TEST_DIMMER_DEVICE_ID, status_update
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state_after = hass.states.get(TEST_DIMMER_ENTITY_ID)
|
||||
assert state_after is not None
|
||||
assert state_after.state == "on"
|
||||
assert state_after.attributes.get("brightness") == 128
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -168,10 +177,28 @@ async def test_dispatcher_connection(
|
||||
async def test_status_updates(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_gateway: MagicMock,
|
||||
mock_devices: list[MagicMock],
|
||||
device_id: str,
|
||||
status_update: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test various status updates for different device types."""
|
||||
_dispatch_status(mock_gateway, device_id, status_update)
|
||||
device = next(d for d in mock_devices if d.dev_id == device_id)
|
||||
_trigger_light_status_callback(device, device_id, status_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_device_availability(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_devices: list[MagicMock],
|
||||
) -> None:
|
||||
"""Test device availability changes."""
|
||||
_trigger_availability_callback(mock_devices[0], TEST_DIMMER_DEVICE_ID, False)
|
||||
await hass.async_block_till_done()
|
||||
assert (state := hass.states.get(TEST_DIMMER_ENTITY_ID))
|
||||
assert state.state == "unavailable"
|
||||
|
||||
_trigger_availability_callback(mock_devices[0], TEST_DIMMER_DEVICE_ID, True)
|
||||
await hass.async_block_till_done()
|
||||
assert (state := hass.states.get(TEST_DIMMER_ENTITY_ID))
|
||||
assert state.state != "unavailable"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -12,281 +13,11 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
DEVICE_MOCKS = [
|
||||
"bzyd_45idzfufidgee7ir", # https://github.com/orgs/home-assistant/discussions/717
|
||||
"bzyd_ssimhf6r8kgwepfb", # https://github.com/orgs/home-assistant/discussions/718
|
||||
"cjkg_hmabvy81", # https://github.com/orgs/home-assistant/discussions/1297
|
||||
"cjkg_uenof8jd", # https://github.com/home-assistant/core/issues/151825
|
||||
"ckmkzq_1yyqfw4djv9eii3q", # https://github.com/home-assistant/core/issues/150856
|
||||
"cl_3r8gc33pnqsxfe1g", # https://github.com/tuya/tuya-home-assistant/issues/754
|
||||
"cl_669wsr2w4cvinbh4", # https://github.com/home-assistant/core/issues/150856
|
||||
"cl_cpbo62rn", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"cl_ebt12ypvexnixvtf", # https://github.com/tuya/tuya-home-assistant/issues/754
|
||||
"cl_g1cp07dsqnbdbbki", # https://github.com/home-assistant/core/issues/139966
|
||||
"cl_lfkr93x0ukp5gaia", # https://github.com/home-assistant/core/issues/152826
|
||||
"cl_n3xgr5pdmpinictg", # https://github.com/home-assistant/core/issues/153537
|
||||
"cl_qqdxfdht", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"cl_rD7uqAAgQOpSA2Rx", # https://github.com/home-assistant/core/issues/139966
|
||||
"cl_zah67ekd", # https://github.com/home-assistant/core/issues/71242
|
||||
"clkg_nhyj64w2", # https://github.com/home-assistant/core/issues/136055
|
||||
"clkg_wltqkykhni0papzj", # https://github.com/home-assistant/core/issues/151635
|
||||
"clkg_xqvhthwkbmp3aghs", # https://github.com/home-assistant/core/issues/139966
|
||||
"co2bj_yakol79dibtswovc", # https://github.com/home-assistant/core/issues/151784
|
||||
"co2bj_yrr3eiyiacm31ski", # https://github.com/orgs/home-assistant/discussions/842
|
||||
"cobj_hcdy5zrq3ikzthws", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"cs_b9oyi2yofflroq1g", # https://github.com/home-assistant/core/issues/139966
|
||||
"cs_eguoms25tkxtf5u8", # https://github.com/home-assistant/core/issues/152361
|
||||
"cs_ipmyy4nigpqcnd8q", # https://github.com/home-assistant/core/pull/148726
|
||||
"cs_ka2wfrdoogpvgzfi", # https://github.com/home-assistant/core/issues/119865
|
||||
"cs_qhxmvae667uap4zh", # https://github.com/home-assistant/core/issues/141278
|
||||
"cs_vmxuxszzjwp5smli", # https://github.com/home-assistant/core/issues/119865
|
||||
"cs_u0wirz487erb0eka", # https://github.com/home-assistant/core/issues/155364
|
||||
"cs_zibqa9dutqyaxym2", # https://github.com/home-assistant/core/pull/125098
|
||||
"cwjwq_agwu93lr", # https://github.com/orgs/home-assistant/discussions/79
|
||||
"cwwsq_lxfvx41gqdotrkgi", # https://github.com/orgs/home-assistant/discussions/730
|
||||
"cwwsq_wfkzyy0evslzsmoi", # https://github.com/home-assistant/core/issues/144745
|
||||
"cwysj_akln8rb04cav403q", # https://github.com/home-assistant/core/pull/146599
|
||||
"cwysj_z3rpyvznfcch99aa", # https://github.com/home-assistant/core/pull/146599
|
||||
"cz_0fHWRe8ULjtmnBNd", # https://github.com/home-assistant/core/issues/139966
|
||||
"cz_0g1fmqh6d5io7lcn", # https://github.com/home-assistant/core/issues/149704
|
||||
"cz_2iepauebcvo74ujc", # https://github.com/home-assistant/core/issues/141278
|
||||
"cz_2jxesipczks0kdct", # https://github.com/home-assistant/core/issues/147149
|
||||
"cz_37mnhia3pojleqfh", # https://github.com/home-assistant/core/issues/146164
|
||||
"cz_39sy2g68gsjwo2xv", # https://github.com/home-assistant/core/issues/141278
|
||||
"cz_4mbgrfotyNzMxDAv", # https://github.com/home-assistant/core/issues/152295
|
||||
"cz_6fa7odsufen374x2", # https://github.com/home-assistant/core/issues/150029
|
||||
"cz_79a7z01v3n35kytb", # https://github.com/orgs/home-assistant/discussions/221
|
||||
"cz_9ivirni8wemum6cw", # https://github.com/home-assistant/core/issues/139735
|
||||
"cz_AiHXxAyyn7eAkLQY", # https://github.com/home-assistant/core/issues/150662
|
||||
"cz_CHLZe9HQ6QIXujVN", # https://github.com/home-assistant/core/issues/149233
|
||||
"cz_HBRBzv1UVBVfF6SL", # https://github.com/tuya/tuya-home-assistant/issues/754
|
||||
"cz_IGzCi97RpN2Lf9cu", # https://github.com/home-assistant/core/issues/139966
|
||||
"cz_PGEkBctAbtzKOZng", # https://github.com/home-assistant/core/issues/139966
|
||||
"cz_anwgf2xugjxpkfxb", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"cz_cuhokdii7ojyw8k2", # https://github.com/home-assistant/core/issues/149704
|
||||
"cz_dhto3y4uachr1wll", # https://github.com/orgs/home-assistant/discussions/169
|
||||
"cz_dntgh2ngvshfxpsz", # https://github.com/home-assistant/core/issues/149704
|
||||
"cz_fencxse0bnut96ig", # https://github.com/home-assistant/core/issues/63978
|
||||
"cz_gbtxrqfy9xcsakyp", # https://github.com/home-assistant/core/issues/141278
|
||||
"cz_gjnozsaz", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"cz_hA2GsgMfTQFTz9JL", # https://github.com/home-assistant/core/issues/148347
|
||||
"cz_hj0a5c7ckzzexu8l", # https://github.com/home-assistant/core/issues/149704
|
||||
"cz_ik9sbig3mthx9hjz", # https://github.com/home-assistant/core/issues/141278
|
||||
"cz_ipabufmlmodje1ws", # https://github.com/home-assistant/core/issues/63978
|
||||
"cz_iqhidxhhmgxk5eja", # https://github.com/home-assistant/core/issues/149233
|
||||
"cz_jnbbxsb84gvvyfg5", # https://github.com/tuya/tuya-home-assistant/issues/754
|
||||
"cz_jti3ce2hzvsposgj", # https://github.com/home-assistant/core/issues/152295
|
||||
"cz_mQUhiTg9kwydBFBd", # https://github.com/home-assistant/core/issues/139966
|
||||
"cz_n8iVBAPLFKAAAszH", # https://github.com/home-assistant/core/issues/146164
|
||||
"cz_nkb0fmtlfyqosnvk", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"cz_nx8rv6jpe1tsnffk", # https://github.com/home-assistant/core/issues/148347
|
||||
"cz_piuensvr", # https://github.com/home-assistant/core/issues/139966
|
||||
"cz_qm0iq4nqnrlzh4qc", # https://github.com/home-assistant/core/issues/141278
|
||||
"cz_qxJSyTLEtX5WrzA9", # https://github.com/home-assistant/core/issues/139966
|
||||
"cz_raceucn29wk2yawe", # https://github.com/tuya/tuya-home-assistant/issues/754
|
||||
"cz_sb6bwb1n8ma2c5q4", # https://github.com/home-assistant/core/issues/141278
|
||||
"cz_t0a4hwsf8anfsadp", # https://github.com/home-assistant/core/issues/149704
|
||||
"cz_tf6qp8t3hl9h7m94", # https://github.com/home-assistant/core/issues/143209
|
||||
"cz_tkn2s79mzedk6pwr", # https://github.com/home-assistant/core/issues/146164
|
||||
"cz_vrbpx6h7fsi5mujb", # https://github.com/home-assistant/core/pull/149234
|
||||
"cz_vxqn72kwtosoy4d3", # https://github.com/home-assistant/core/issues/141278
|
||||
"cz_w0qqde0g", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"cz_wifvoilfrqeo6hvu", # https://github.com/home-assistant/core/issues/146164
|
||||
"cz_wrz6vzch8htux2zp", # https://github.com/home-assistant/core/issues/141278
|
||||
"cz_y4jnobxh", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"cz_yncyws7tu1q4cpsz", # https://github.com/home-assistant/core/issues/150662
|
||||
"cz_z6pht25s3p0gs26q", # https://github.com/home-assistant/core/issues/63978
|
||||
"dc_l3bpgg8ibsagon4x", # https://github.com/home-assistant/core/issues/149704
|
||||
"dd_gaobbrxqiblcng2p", # https://github.com/home-assistant/core/issues/149233
|
||||
"dj_0gyaslysqfp4gfis", # https://github.com/home-assistant/core/issues/149895
|
||||
"dj_8szt7whdvwpmxglk", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_8ugheslg", # https://github.com/home-assistant/core/issues/150856
|
||||
"dj_8y0aquaa8v6tho8w", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_AqHUMdcbYzIq1Of4", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"dj_amx1bgdrfab6jngb", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"dj_bSXSSFArVKtc4DyC", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"dj_baf9tt9lb8t5uc7z", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_c3nsqogqovapdpfj", # https://github.com/home-assistant/core/issues/146164
|
||||
"dj_d4g0fbsoaal841o6", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_dbou1ap4", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"dj_djnozmdyqyriow8z", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_ekwolitfjhxn55js", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_fuupmcr2mb1odkja", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_h4aX2JkHZNByQ4AV", # https://github.com/home-assistant/core/issues/150662
|
||||
"dj_hp6orhaqm6as3jnv", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_hpc8ddyfv85haxa7", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_iayz2jmtlipjnxj7", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_idnfq7xbx8qewyoa", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_ilddqqih3tucdk68", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_j1bgp31cffutizub", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_kgaob37tz2muf3mi", # https://github.com/home-assistant/core/issues/150856
|
||||
"dj_lmnt3uyltk1xffrt", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_mki13ie507rlry4r", # https://github.com/home-assistant/core/pull/126242
|
||||
"dj_nbumqpv8vz61enji", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_nlxvjzy1hoeiqsg6", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_oe0cpnjg", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_qoqolwtqzfuhgghq", # https://github.com/home-assistant/core/issues/149233
|
||||
"dj_riwp3k79", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_tgewj70aowigv8fz", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"dj_tmsloaroqavbucgn", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_ufq2xwuzd4nb0qdr", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_vqwcnabamzrc2kab", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_xdvitmhhmgefaeuq", # https://github.com/home-assistant/core/issues/146164
|
||||
"dj_xokdfs6kh5ednakk", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_zakhnlpdiu0ycdxn", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_zav1pa32pyxray78", # https://github.com/home-assistant/core/issues/149704
|
||||
"dj_zputiamzanuk6yky", # https://github.com/home-assistant/core/issues/149704
|
||||
"dlq_0tnvg2xaisqdadcf", # https://github.com/home-assistant/core/issues/102769
|
||||
"dlq_cnpkf4xdmd9v49iq", # https://github.com/home-assistant/core/pull/149320
|
||||
"dlq_dikb3dp6", # https://github.com/home-assistant/core/pull/151601
|
||||
"dlq_jdj6ccklup7btq3a", # https://github.com/home-assistant/core/issues/143209
|
||||
"dlq_kxdr6su0c55p7bbo", # https://github.com/home-assistant/core/issues/143499
|
||||
"dlq_r9kg2g1uhhyicycb", # https://github.com/home-assistant/core/issues/149650
|
||||
"dlq_z3jngbyubvwgfrcv", # https://github.com/home-assistant/core/issues/150293
|
||||
"dr_pjvxl1wsyqxivsaf", # https://github.com/home-assistant/core/issues/84869
|
||||
"fs_g0ewlb1vmwqljzji", # https://github.com/home-assistant/core/issues/141231
|
||||
"fs_ibytpo6fpnugft1c", # https://github.com/home-assistant/core/issues/135541
|
||||
"fsd_9ecs16c53uqskxw6", # https://github.com/home-assistant/core/issues/149233
|
||||
"gyd_lgekqfxdabipm3tn", # https://github.com/home-assistant/core/issues/133173
|
||||
"hps_2aaelwxk", # https://github.com/home-assistant/core/issues/149704
|
||||
"hps_wqashyqo", # https://github.com/home-assistant/core/issues/146180
|
||||
"hwsb_ircs2n82vgrozoew", # https://github.com/home-assistant/core/issues/149233
|
||||
"jsq_r492ifwk6f2ssptb", # https://github.com/home-assistant/core/issues/151488
|
||||
"jtmspro_xqeob8h6", # https://github.com/orgs/home-assistant/discussions/517
|
||||
"kg_4nqs33emdwJxpQ8O", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"kg_5ftkaulg", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"kg_gbm9ata1zrzaez4a", # https://github.com/home-assistant/core/issues/148347
|
||||
"kj_CAjWAxBUZt7QZHfz", # https://github.com/home-assistant/core/issues/146023
|
||||
"kj_fsxtzzhujkrak2oy", # https://github.com/orgs/home-assistant/discussions/439
|
||||
"kj_s4uzibibgzdxzowo", # https://github.com/home-assistant/core/issues/150246
|
||||
"kj_yrzylxax1qspdgpp", # https://github.com/orgs/home-assistant/discussions/61
|
||||
"ks_j9fa8ahzac8uvlfl", # https://github.com/orgs/home-assistant/discussions/329
|
||||
"kt_5wnlzekkstwcdsvm", # https://github.com/home-assistant/core/pull/148646
|
||||
"kt_ibmmirhhq62mmf1g", # https://github.com/home-assistant/core/pull/150077
|
||||
"kt_vdadlnmsorlhw4td", # https://github.com/home-assistant/core/pull/149635
|
||||
"ldcg_9kbbfeho", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"mal_gyitctrjj1kefxp2", # Alarm Host support
|
||||
"mc_oSQljE9YDqwCwTUA", # https://github.com/home-assistant/core/issues/149233
|
||||
"mcs_6ywsnauy", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"mcs_7jIGJAymiH8OsFFb", # https://github.com/home-assistant/core/issues/108301
|
||||
"mcs_8yhypbo7", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"mcs_hx5ztlztij4yxxvg", # https://github.com/home-assistant/core/issues/148347
|
||||
"mcs_oxslv1c9", # https://github.com/home-assistant/core/issues/139966
|
||||
"mcs_qxu3flpqjsc1kqu3", # https://github.com/home-assistant/core/issues/141278
|
||||
"msp_3ddulzljdjjwkhoy", # https://github.com/orgs/home-assistant/discussions/262
|
||||
"msp_mzuro9jgjs7uiryn", # https://github.com/home-assistant/core/pull/156048
|
||||
"mzj_jlapoy5liocmtdvd", # https://github.com/home-assistant/core/issues/150662
|
||||
"mzj_qavcakohisj5adyh", # https://github.com/home-assistant/core/issues/141278
|
||||
"ntq_9mqdhwklpvnnvb7t", # https://github.com/orgs/home-assistant/discussions/517
|
||||
"pc_t2afic7i3v1bwhfp", # https://github.com/home-assistant/core/issues/149704
|
||||
"pc_trjopo1vdlt9q1tg", # https://github.com/home-assistant/core/issues/149704
|
||||
"pc_tsbguim4trl6fa7g", # https://github.com/home-assistant/core/issues/146164
|
||||
"pc_yku9wsimasckdt15", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"pir_3amxzozho9xp4mkh", # https://github.com/home-assistant/core/issues/149704
|
||||
"pir_fcdjzz3s", # https://github.com/home-assistant/core/issues/149704
|
||||
"pir_j5jgnjvdaczeb6dc", # https://github.com/orgs/home-assistant/discussions/582
|
||||
"pir_wqz93nrdomectyoz", # https://github.com/home-assistant/core/issues/149704
|
||||
"qccdz_7bvgooyjhiua1yyq", # https://github.com/home-assistant/core/issues/136207
|
||||
"qn_5ls2jw49hpczwqng", # https://github.com/home-assistant/core/issues/149233
|
||||
"qt_TtXKwTMwiPpURWLJ", # https://github.com/home-assistant/core/issues/139966
|
||||
"qxj_fsea1lat3vuktbt6", # https://github.com/orgs/home-assistant/discussions/318
|
||||
"qxj_is2indt9nlth6esa", # https://github.com/home-assistant/core/issues/136472
|
||||
"qxj_xbwbniyt6bgws9ia", # https://github.com/orgs/home-assistant/discussions/823
|
||||
"rqbj_4iqe2hsfyd86kwwc", # https://github.com/orgs/home-assistant/discussions/100
|
||||
"rs_d7woucobqi8ncacf", # https://github.com/orgs/home-assistant/discussions/1021
|
||||
"sd_i6hyjg3af7doaswm", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"sd_lr33znaodtyarrrz", # https://github.com/home-assistant/core/issues/141278
|
||||
"sfkzq_1fcnd8xk", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"sfkzq_d4vpmigg", # https://github.com/home-assistant/core/issues/150662
|
||||
"sfkzq_ed7frwissyqrejic", # https://github.com/home-assistant/core/pull/149236
|
||||
"sfkzq_nxquc5lb", # https://github.com/home-assistant/core/issues/150662
|
||||
"sfkzq_o6dagifntoafakst", # https://github.com/home-assistant/core/issues/148116
|
||||
"sfkzq_rzklytdei8i8vo37", # https://github.com/home-assistant/core/issues/146164
|
||||
"sgbj_DYgId0sz6zWlmmYu", # https://github.com/orgs/home-assistant/discussions/583
|
||||
"sgbj_im2eqqhj72suwwko", # https://github.com/home-assistant/core/issues/151082
|
||||
"sgbj_ulv4nnue7gqp0rjk", # https://github.com/home-assistant/core/issues/149704
|
||||
"sj_rzeSU2h9uoklxEwq", # https://github.com/home-assistant/core/issues/150683
|
||||
"sj_tgvtvdoc", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"sjz_ftbc8rp8ipksdfpv", # https://github.com/orgs/home-assistant/discussions/51
|
||||
"sp_6bmk1remyscwyx6i", # https://github.com/orgs/home-assistant/discussions/842
|
||||
"sp_csr2fqitalj5o0tq", # https://github.com/home-assistant/core/pull/156103
|
||||
"sp_drezasavompxpcgm", # https://github.com/home-assistant/core/issues/149704
|
||||
"sp_nzauwyj3mcnjnf35", # https://github.com/home-assistant/core/issues/141278
|
||||
"sp_rjKXWRohlvOTyLBu", # https://github.com/home-assistant/core/issues/149704
|
||||
"sp_rudejjigkywujjvs", # https://github.com/home-assistant/core/issues/146164
|
||||
"sp_sdd5f5f2dl5wydjf", # https://github.com/home-assistant/core/issues/144087
|
||||
"swtz_3rzngbyy", # https://github.com/orgs/home-assistant/discussions/688
|
||||
"szjcy_u5xgcpcngk3pfxb4", # https://github.com/orgs/home-assistant/discussions/934
|
||||
"tdq_1aegphq4yfd50e6b", # https://github.com/home-assistant/core/issues/143209
|
||||
"tdq_9htyiowaf5rtdhrv", # https://github.com/home-assistant/core/issues/143209
|
||||
"tdq_cq1p0nt0a4rixnex", # https://github.com/home-assistant/core/issues/146845
|
||||
"tdq_nockvv2k39vbrxxk", # https://github.com/home-assistant/core/issues/145849
|
||||
"tdq_p6sqiuesvhmhvv4f", # https://github.com/orgs/home-assistant/discussions/430
|
||||
"tdq_pu8uhxhwcp3tgoz7", # https://github.com/home-assistant/core/issues/141278
|
||||
"tdq_uoa3mayicscacseb", # https://github.com/home-assistant/core/issues/128911
|
||||
"tdq_x3o8epevyeo3z3oa", # https://github.com/orgs/home-assistant/discussions/430
|
||||
"tyndj_pyakuuoc", # https://github.com/home-assistant/core/issues/149704
|
||||
"wfcon_b25mh8sxawsgndck", # https://github.com/home-assistant/core/issues/149704
|
||||
"wfcon_lieerjyy6l4ykjor", # https://github.com/home-assistant/core/issues/136055
|
||||
"wfcon_plp0gnfcacdeqk5o", # https://github.com/home-assistant/core/issues/139966
|
||||
"wg2_2gowdgni", # https://github.com/home-assistant/core/issues/150856
|
||||
"wg2_haclbl0qkqlf2qds", # https://github.com/orgs/home-assistant/discussions/517
|
||||
"wg2_nwxr8qcu4seltoro", # https://github.com/orgs/home-assistant/discussions/430
|
||||
"wg2_setmxeqgs63xwopm", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"wg2_tmwhss6ntjfc7prs", # https://github.com/home-assistant/core/issues/150662
|
||||
"wg2_v7owd9tzcaninc36", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"wk_6kijc7nd", # https://github.com/home-assistant/core/issues/136513
|
||||
"wk_IAYz2WK1th0cMLmL", # https://github.com/home-assistant/core/issues/150077
|
||||
"wk_aqoouq7x", # https://github.com/home-assistant/core/issues/146263
|
||||
"wk_ccpwojhalfxryigz", # https://github.com/home-assistant/core/issues/145551
|
||||
"wk_cpmgn2cf", # https://github.com/orgs/home-assistant/discussions/684
|
||||
"wk_fi6dne5tu4t1nm6j", # https://github.com/orgs/home-assistant/discussions/243
|
||||
"wk_gc1bxoq2hafxpa35", # https://github.com/home-assistant/core/issues/145551
|
||||
"wk_gogb05wrtredz3bs", # https://github.com/home-assistant/core/issues/136337
|
||||
"wk_t94pit6zjbask9qo", # https://github.com/home-assistant/core/pull/156781
|
||||
"wk_tfbhw0mg", # https://github.com/home-assistant/core/issues/152282
|
||||
"wk_y5obtqhuztqsf2mj", # https://github.com/home-assistant/core/issues/139735
|
||||
"wkcz_gc4b1mdw7kebtuyz", # https://github.com/home-assistant/core/issues/135617
|
||||
"wkf_9xfjixap", # https://github.com/home-assistant/core/issues/139966
|
||||
"wkf_p3dbf6qs", # https://github.com/home-assistant/core/issues/139966
|
||||
"wnykq_kzwdw5bpxlbs9h9g", # https://github.com/orgs/home-assistant/discussions/842
|
||||
"wnykq_npbbca46yiug8ysk", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"wnykq_om518smspsaltzdi", # https://github.com/home-assistant/core/issues/150662
|
||||
"wnykq_rqhxdyusjrwxyff6", # https://github.com/home-assistant/core/issues/133173
|
||||
"wsdcg_g2y6z3p3ja2qhyav", # https://github.com/home-assistant/core/issues/102769
|
||||
"wsdcg_iq4ygaai", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"wsdcg_iv7hudlj", # https://github.com/home-assistant/core/issues/141278
|
||||
"wsdcg_krlcihrpzpc8olw9", # https://github.com/orgs/home-assistant/discussions/517
|
||||
"wsdcg_lf36y5nwb8jkxwgg", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"wsdcg_qrztc3ev", # https://github.com/home-assistant/core/issues/139966
|
||||
"wsdcg_vtA4pDd6PLUZzXgZ", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"wsdcg_xr3htd96", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"wsdcg_yqiqbaldtr0i7mru", # https://github.com/home-assistant/core/issues/136223
|
||||
"wxkg_ja5osu5g", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"wxkg_l8yaz4um5b3pwyvf", # https://github.com/home-assistant/core/issues/93975
|
||||
"wxnbq_5l1ht8jygsyr1wn1", # https://github.com/orgs/home-assistant/discussions/685
|
||||
"xdd_shx9mmadyyeaq88t", # https://github.com/home-assistant/core/issues/151141
|
||||
"xnyjcn_pb0tc75khaik8qbg", # https://github.com/home-assistant/core/pull/149237
|
||||
"ydkt_jevroj5aguwdbs2e", # https://github.com/orgs/home-assistant/discussions/288
|
||||
"ygsb_l6ax0u6jwbz82atk", # https://github.com/home-assistant/core/issues/146319
|
||||
"ykq_bngwdjsr", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"ywbj_arywmw6h6vesoz5t", # https://github.com/home-assistant/core/issues/146164
|
||||
"ywbj_cjlutkuuvxnie17o", # https://github.com/home-assistant/core/issues/146164
|
||||
"ywbj_gf9dejhmzffgdyfj", # https://github.com/home-assistant/core/issues/149704
|
||||
"ywbj_kscbebaf3s1eogvt", # https://github.com/home-assistant/core/issues/141278
|
||||
"ywbj_rccxox8p", # https://github.com/orgs/home-assistant/discussions/625
|
||||
"ywcgq_h8lvyoahr6s6aybf", # https://github.com/home-assistant/core/issues/145932
|
||||
"ywcgq_wtzwyhkev3b4ubns", # https://github.com/home-assistant/core/issues/103818
|
||||
"zjq_nkkl7uzv", # https://github.com/orgs/home-assistant/discussions/482
|
||||
"zndb_4ggkyflayu1h1ho9", # https://github.com/home-assistant/core/pull/149317
|
||||
"zndb_v5jlnn5hwyffkhp3", # https://github.com/home-assistant/core/issues/143209
|
||||
"zndb_ze8faryrxr0glqnn", # https://github.com/home-assistant/core/issues/138372
|
||||
"znnbq_0kllybtbzftaee7y", # https://github.com/orgs/home-assistant/discussions/685
|
||||
"znnbq_6b3pbbuqbfabhfiq", # https://github.com/orgs/home-assistant/discussions/707
|
||||
"znrb_db81ge24jctwx8lo", # https://github.com/home-assistant/core/issues/136513
|
||||
"zwjcy_gvygg3m8", # https://github.com/orgs/home-assistant/discussions/949
|
||||
"zwjcy_myd45weu", # https://github.com/orgs/home-assistant/discussions/482
|
||||
]
|
||||
FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures"
|
||||
DEVICE_MOCKS = sorted(
|
||||
str(path.relative_to(FIXTURES_DIR).with_suffix(""))
|
||||
for path in FIXTURES_DIR.glob("*.json")
|
||||
)
|
||||
|
||||
|
||||
class MockDeviceListener(DeviceListener):
|
||||
|
||||
@@ -10376,12 +10376,14 @@
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.kattenbak_excretion_time_day-entry]
|
||||
# name: test_platform_setup_and_discovery[sensor.kattenbak_excretion_duration-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -10389,7 +10391,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.kattenbak_excretion_time_day',
|
||||
'entity_id': 'sensor.kattenbak_excretion_duration',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -10404,7 +10406,7 @@
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Excretion time (day)',
|
||||
'original_name': 'Excretion duration',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -10414,15 +10416,16 @@
|
||||
'unit_of_measurement': 's',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.kattenbak_excretion_time_day-state]
|
||||
# name: test_platform_setup_and_discovery[sensor.kattenbak_excretion_duration-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'Kattenbak Excretion time (day)',
|
||||
'friendly_name': 'Kattenbak Excretion duration',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 's',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.kattenbak_excretion_time_day',
|
||||
'entity_id': 'sensor.kattenbak_excretion_duration',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
@@ -10478,6 +10481,54 @@
|
||||
'state': '1.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.kattenbak_status-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': None,
|
||||
'entity_id': 'sensor.kattenbak_status',
|
||||
'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': 'Status',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cat_litter_box_status',
|
||||
'unique_id': 'tuya.yohkwjjdjlzludd3psmstatus',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.kattenbak_status-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Kattenbak Status',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.kattenbak_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'standby',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.keller_current-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -15243,12 +15294,14 @@
|
||||
'state': '3.6',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.poopy_nano_2_excretion_time_day-entry]
|
||||
# name: test_platform_setup_and_discovery[sensor.poopy_nano_2_excretion_duration-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -15256,7 +15309,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.poopy_nano_2_excretion_time_day',
|
||||
'entity_id': 'sensor.poopy_nano_2_excretion_duration',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -15271,7 +15324,7 @@
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Excretion time (day)',
|
||||
'original_name': 'Excretion duration',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -15281,15 +15334,16 @@
|
||||
'unit_of_measurement': 's',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.poopy_nano_2_excretion_time_day-state]
|
||||
# name: test_platform_setup_and_discovery[sensor.poopy_nano_2_excretion_duration-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'Poopy Nano 2 Excretion time (day)',
|
||||
'friendly_name': 'Poopy Nano 2 Excretion duration',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 's',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.poopy_nano_2_excretion_time_day',
|
||||
'entity_id': 'sensor.poopy_nano_2_excretion_duration',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
@@ -15345,6 +15399,54 @@
|
||||
'state': '4.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.poopy_nano_2_status-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': None,
|
||||
'entity_id': 'sensor.poopy_nano_2_status',
|
||||
'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': 'Status',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cat_litter_box_status',
|
||||
'unique_id': 'tuya.nyriu7sjgj9oruzmpsmstatus',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.poopy_nano_2_status-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Poopy Nano 2 Status',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.poopy_nano_2_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.production_total_energy-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -1885,6 +1885,62 @@
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_link_speed-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.wired_client_link_speed',
|
||||
'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.DATA_RATE: 'data_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Link speed',
|
||||
'platform': 'unifi',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'wired_client_link_speed',
|
||||
'unique_id': 'wired_speed-00:00:00:00:00:01',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_link_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_rate',
|
||||
'friendly_name': 'Wired client Link speed',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.wired_client_link_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1000',
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_rx-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -55,6 +55,7 @@ WIRED_CLIENT = {
|
||||
"wired-rx_bytes-r": 1234000000,
|
||||
"wired-tx_bytes-r": 5678000000,
|
||||
"uptime": 1600094505,
|
||||
"wired_rate_mbps": 1000,
|
||||
}
|
||||
WIRELESS_CLIENT = {
|
||||
"is_wired": False,
|
||||
@@ -542,6 +543,42 @@ async def test_bandwidth_sensors(
|
||||
assert hass.states.get("sensor.wired_client_tx")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("client_payload", [[WIRED_CLIENT]])
|
||||
@pytest.mark.usefixtures("config_entry_setup")
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_wired_client_speed_sensor(
|
||||
hass: HomeAssistant,
|
||||
mock_websocket_message: WebsocketMessageMock,
|
||||
client_payload: list[dict[str, Any]],
|
||||
) -> None:
|
||||
"""Verify that wired client speed sensor is working as expected."""
|
||||
# Verify sensor is created and has correct state
|
||||
assert hass.states.get("sensor.wired_client_link_speed").state == "1000"
|
||||
|
||||
# Verify state update
|
||||
wired_client = deepcopy(client_payload[0])
|
||||
wired_client["wired_rate_mbps"] = 2500
|
||||
|
||||
mock_websocket_message(message=MessageKey.CLIENT, data=wired_client)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.wired_client_link_speed").state == "2500"
|
||||
|
||||
# Verify sensor is unavailable when client disconnects
|
||||
new_time = dt_util.utcnow()
|
||||
wired_client["last_seen"] = dt_util.as_timestamp(new_time)
|
||||
|
||||
mock_websocket_message(message=MessageKey.CLIENT, data=wired_client)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
new_time += timedelta(seconds=(DEFAULT_DETECTION_TIME + 1))
|
||||
with freeze_time(new_time):
|
||||
async_fire_time_changed(hass, new_time)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.wired_client_link_speed").state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config_entry_options",
|
||||
[{CONF_ALLOW_BANDWIDTH_SENSORS: True, CONF_ALLOW_UPTIME_SENSORS: True}],
|
||||
@@ -555,9 +592,10 @@ async def test_remove_sensors(
|
||||
client_payload: list[dict[str, Any]],
|
||||
) -> None:
|
||||
"""Verify removing of clients work as expected."""
|
||||
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6
|
||||
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 7
|
||||
assert hass.states.get("sensor.wired_client_rx")
|
||||
assert hass.states.get("sensor.wired_client_tx")
|
||||
assert hass.states.get("sensor.wired_client_link_speed")
|
||||
assert hass.states.get("sensor.wired_client_uptime")
|
||||
assert hass.states.get("sensor.wireless_client_rx")
|
||||
assert hass.states.get("sensor.wireless_client_tx")
|
||||
@@ -570,6 +608,7 @@ async def test_remove_sensors(
|
||||
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
|
||||
assert hass.states.get("sensor.wired_client_rx") is None
|
||||
assert hass.states.get("sensor.wired_client_tx") is None
|
||||
assert hass.states.get("sensor.wired_client_link_speed") is None
|
||||
assert hass.states.get("sensor.wired_client_uptime") is None
|
||||
assert hass.states.get("sensor.wireless_client_rx")
|
||||
assert hass.states.get("sensor.wireless_client_tx")
|
||||
|
||||
1
tests/components/victron_ble/__init__.py
Normal file
1
tests/components/victron_ble/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the Victron Bluetooth Low Energy integration."""
|
||||
75
tests/components/victron_ble/conftest.py
Normal file
75
tests/components/victron_ble/conftest.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Test the Victron Bluetooth Low Energy config flow."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from home_assistant_bluetooth import BluetoothServiceInfo
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.victron_ble.const import DOMAIN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .fixtures import VICTRON_VEBUS_SERVICE_INFO, VICTRON_VEBUS_TOKEN
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.victron_ble.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_discovered_service_info() -> Generator[AsyncMock]:
|
||||
"""Mock discovered service info."""
|
||||
with patch(
|
||||
"homeassistant.components.victron_ble.config_flow.async_discovered_service_info",
|
||||
return_value=[VICTRON_VEBUS_SERVICE_INFO],
|
||||
) as mock_discovered_service_info:
|
||||
yield mock_discovered_service_info
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service_info() -> BluetoothServiceInfo:
|
||||
"""Return service info."""
|
||||
return VICTRON_VEBUS_SERVICE_INFO
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def access_token() -> str:
|
||||
"""Return access token."""
|
||||
return VICTRON_VEBUS_TOKEN
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(
|
||||
service_info: BluetoothServiceInfo, access_token: str
|
||||
) -> MockConfigEntry:
|
||||
"""Mock config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_ADDRESS: service_info.address,
|
||||
CONF_ACCESS_TOKEN: access_token,
|
||||
},
|
||||
unique_id=service_info.address,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry_added_to_hass(
|
||||
mock_config_entry,
|
||||
hass: HomeAssistant,
|
||||
service_info: BluetoothServiceInfo,
|
||||
access_token: str,
|
||||
) -> MockConfigEntry:
|
||||
"""Mock config entry factory that added to hass."""
|
||||
|
||||
entry = mock_config_entry
|
||||
entry.add_to_hass(hass)
|
||||
return entry
|
||||
147
tests/components/victron_ble/fixtures.py
Normal file
147
tests/components/victron_ble/fixtures.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""Fixtures for testing victron_ble."""
|
||||
|
||||
from home_assistant_bluetooth import BluetoothServiceInfo
|
||||
|
||||
NOT_VICTRON_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="Not it",
|
||||
address="61DE521B-F0BF-9F44-64D4-75BBE1738105",
|
||||
rssi=-63,
|
||||
manufacturer_data={3234: b"\x00\x01"},
|
||||
service_data={},
|
||||
service_uuids=[],
|
||||
source="local",
|
||||
)
|
||||
|
||||
VICTRON_TEST_WRONG_TOKEN = "00000000000000000000000000000000"
|
||||
|
||||
# battery monitor
|
||||
VICTRON_BATTERY_MONITOR_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="Battery Monitor",
|
||||
address="01:02:03:04:05:07",
|
||||
rssi=-60,
|
||||
manufacturer_data={
|
||||
0x02E1: bytes.fromhex("100289a302b040af925d09a4d89aa0128bdef48c6298a9")
|
||||
},
|
||||
service_data={},
|
||||
service_uuids=[],
|
||||
source="local",
|
||||
)
|
||||
VICTRON_BATTERY_MONITOR_TOKEN = "aff4d0995b7d1e176c0c33ecb9e70dcd"
|
||||
VICTRON_BATTERY_MONITOR_SENSORS = {
|
||||
"battery_monitor_aux_mode": "disabled",
|
||||
"battery_monitor_consumed_ampere_hours": "-50.0",
|
||||
"battery_monitor_current": "0.0",
|
||||
"battery_monitor_remaining_minutes": "unknown",
|
||||
"battery_monitor_state_of_charge": "50.0",
|
||||
"battery_monitor_voltage": "12.53",
|
||||
"battery_monitor_alarm": "none",
|
||||
"battery_monitor_temperature": "unknown",
|
||||
"battery_monitor_starter_voltage": "unknown",
|
||||
"battery_monitor_midpoint_voltage": "unknown",
|
||||
}
|
||||
|
||||
# DC/DC converter
|
||||
|
||||
VICTRON_DC_DC_CONVERTER_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="DC/DC Converter",
|
||||
address="01:02:03:04:05:08",
|
||||
rssi=-60,
|
||||
manufacturer_data={
|
||||
0x02E1: bytes.fromhex("1000c0a304121d64ca8d442b90bbdf6a8cba"),
|
||||
},
|
||||
service_data={},
|
||||
service_uuids=[],
|
||||
source="local",
|
||||
)
|
||||
|
||||
# DC energy meter
|
||||
|
||||
VICTRON_DC_ENERGY_METER_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="DC Energy Meter",
|
||||
address="01:02:03:04:05:09",
|
||||
rssi=-60,
|
||||
manufacturer_data={
|
||||
0x02E1: bytes.fromhex("100289a30d787fafde83ccec982199fd815286"),
|
||||
},
|
||||
service_data={},
|
||||
service_uuids=[],
|
||||
source="local",
|
||||
)
|
||||
|
||||
VICTRON_DC_ENERGY_METER_TOKEN = "aff4d0995b7d1e176c0c33ecb9e70dcd"
|
||||
|
||||
VICTRON_DC_ENERGY_METER_SENSORS = {
|
||||
"dc_energy_meter_meter_type": "dc_dc_charger",
|
||||
"dc_energy_meter_aux_mode": "starter_voltage",
|
||||
"dc_energy_meter_current": "0.0",
|
||||
"dc_energy_meter_voltage": "12.52",
|
||||
"dc_energy_meter_starter_voltage": "-0.01",
|
||||
"dc_energy_meter_alarm": "none",
|
||||
"dc_energy_meter_temperature": "unknown",
|
||||
}
|
||||
|
||||
# Inverter
|
||||
|
||||
VICTRON_INVERTER_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="Inverter",
|
||||
address="01:02:03:04:05:10",
|
||||
rssi=-60,
|
||||
manufacturer_data={
|
||||
0x02E1: bytes.fromhex("1003a2a2031252dad26f0b8eb39162074d140df410"),
|
||||
}, # not a valid advertisement, but model id mangled to match inverter
|
||||
service_data={},
|
||||
service_uuids=[],
|
||||
source="local",
|
||||
)
|
||||
|
||||
# Solar charger
|
||||
|
||||
VICTRON_SOLAR_CHARGER_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="Solar Charger",
|
||||
address="01:02:03:04:05:11",
|
||||
rssi=-60,
|
||||
manufacturer_data={
|
||||
0x02E1: bytes.fromhex("100242a0016207adceb37b605d7e0ee21b24df5c"),
|
||||
},
|
||||
service_data={},
|
||||
service_uuids=[],
|
||||
source="local",
|
||||
)
|
||||
|
||||
VICTRON_SOLAR_CHARGER_TOKEN = "adeccb947395801a4dd45a2eaa44bf17"
|
||||
|
||||
VICTRON_SOLAR_CHARGER_SENSORS = {
|
||||
"solar_charger_charge_state": "absorption",
|
||||
"solar_charger_battery_voltage": "13.88",
|
||||
"solar_charger_battery_current": "1.4",
|
||||
"solar_charger_yield_today": "30",
|
||||
"solar_charger_solar_power": "19",
|
||||
"solar_charger_external_device_load": "0.0",
|
||||
}
|
||||
|
||||
# ve.bus
|
||||
|
||||
VICTRON_VEBUS_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="Inverter Charger",
|
||||
address="01:02:03:04:05:06",
|
||||
rssi=-60,
|
||||
manufacturer_data={
|
||||
0x02E1: bytes.fromhex("100380270c1252dad26f0b8eb39162074d140df410")
|
||||
},
|
||||
service_data={},
|
||||
service_uuids=[],
|
||||
source="local",
|
||||
)
|
||||
|
||||
VICTRON_VEBUS_TOKEN = "da3f5fa2860cb1cf86ba7a6d1d16b9dd"
|
||||
|
||||
VICTRON_VEBUS_SENSORS = {
|
||||
"inverter_charger_device_state": "float",
|
||||
"inverter_charger_battery_voltage": "14.45",
|
||||
"inverter_charger_battery_current": "23.2",
|
||||
"inverter_charger_ac_in_state": "AC_IN_1",
|
||||
"inverter_charger_ac_in_power": "1459",
|
||||
"inverter_charger_ac_out_power": "1046",
|
||||
"inverter_charger_battery_temperature": "32",
|
||||
"inverter_charger_state_of_charge": "unknown",
|
||||
}
|
||||
1891
tests/components/victron_ble/snapshots/test_sensor.ambr
Normal file
1891
tests/components/victron_ble/snapshots/test_sensor.ambr
Normal file
File diff suppressed because it is too large
Load Diff
189
tests/components/victron_ble/test_config_flow.py
Normal file
189
tests/components/victron_ble/test_config_flow.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""Test the Victron Bluetooth Low Energy config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from home_assistant_bluetooth import BluetoothServiceInfo
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.victron_ble.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .fixtures import (
|
||||
NOT_VICTRON_SERVICE_INFO,
|
||||
VICTRON_INVERTER_SERVICE_INFO,
|
||||
VICTRON_TEST_WRONG_TOKEN,
|
||||
VICTRON_VEBUS_SERVICE_INFO,
|
||||
VICTRON_VEBUS_TOKEN,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_bluetooth(enable_bluetooth: None) -> None:
|
||||
"""Mock bluetooth for all tests in this module."""
|
||||
|
||||
|
||||
async def test_async_step_bluetooth_valid_device(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test discovery via bluetooth with a valid device."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=VICTRON_VEBUS_SERVICE_INFO,
|
||||
)
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "access_token"
|
||||
|
||||
# test valid access token
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ACCESS_TOKEN: VICTRON_VEBUS_TOKEN},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||
assert result.get("title") == VICTRON_VEBUS_SERVICE_INFO.name
|
||||
flow_result = result.get("result")
|
||||
assert flow_result is not None
|
||||
assert flow_result.unique_id == VICTRON_VEBUS_SERVICE_INFO.address
|
||||
assert flow_result.data == {
|
||||
CONF_ACCESS_TOKEN: VICTRON_VEBUS_TOKEN,
|
||||
}
|
||||
assert set(flow_result.data.keys()) == {CONF_ACCESS_TOKEN}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source", "service_info", "expected_reason"),
|
||||
[
|
||||
(
|
||||
SOURCE_BLUETOOTH,
|
||||
NOT_VICTRON_SERVICE_INFO,
|
||||
"not_supported",
|
||||
),
|
||||
(
|
||||
SOURCE_BLUETOOTH,
|
||||
VICTRON_INVERTER_SERVICE_INFO,
|
||||
"not_supported",
|
||||
),
|
||||
(
|
||||
SOURCE_USER,
|
||||
None,
|
||||
"no_devices_found",
|
||||
),
|
||||
],
|
||||
ids=["bluetooth_not_victron", "bluetooth_unsupported_device", "user_no_devices"],
|
||||
)
|
||||
async def test_abort_scenarios(
|
||||
hass: HomeAssistant,
|
||||
source: str,
|
||||
service_info: BluetoothServiceInfo | None,
|
||||
expected_reason: str,
|
||||
) -> None:
|
||||
"""Test flows that result in abort."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": source},
|
||||
data=service_info,
|
||||
)
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == expected_reason
|
||||
|
||||
|
||||
async def test_async_step_user_with_devices_found(
|
||||
hass: HomeAssistant, mock_discovered_service_info: AsyncMock
|
||||
) -> None:
|
||||
"""Test setup from service info cache with devices found."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ADDRESS: VICTRON_VEBUS_SERVICE_INFO.address},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "access_token"
|
||||
|
||||
# test invalid access token (valid already tested above)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_ACCESS_TOKEN: VICTRON_TEST_WRONG_TOKEN}
|
||||
)
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "invalid_access_token"
|
||||
|
||||
|
||||
async def test_async_step_user_device_added_between_steps(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_discovered_service_info: AsyncMock,
|
||||
) -> None:
|
||||
"""Test abort when the device gets added via another flow between steps."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "user"
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"address": VICTRON_VEBUS_SERVICE_INFO.address},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "already_configured"
|
||||
|
||||
|
||||
async def test_async_step_user_with_found_devices_already_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry_added_to_hass: MockConfigEntry,
|
||||
mock_discovered_service_info: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setup from service info cache with devices found."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "no_devices_found"
|
||||
|
||||
|
||||
async def test_async_step_bluetooth_devices_already_setup(
|
||||
hass: HomeAssistant, mock_config_entry_added_to_hass: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test we can't start a flow if there is already a config entry."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
data=VICTRON_VEBUS_SERVICE_INFO,
|
||||
)
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "already_configured"
|
||||
|
||||
|
||||
async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> None:
|
||||
"""Test we can't start a flow for the same device twice."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
data=VICTRON_VEBUS_SERVICE_INFO,
|
||||
)
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "access_token"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
data=VICTRON_VEBUS_SERVICE_INFO,
|
||||
)
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "already_in_progress"
|
||||
61
tests/components/victron_ble/test_sensor.py
Normal file
61
tests/components/victron_ble/test_sensor.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Test updating sensors in the victron_ble integration."""
|
||||
|
||||
from home_assistant_bluetooth import BluetoothServiceInfo
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .fixtures import (
|
||||
VICTRON_BATTERY_MONITOR_SERVICE_INFO,
|
||||
VICTRON_BATTERY_MONITOR_TOKEN,
|
||||
VICTRON_DC_ENERGY_METER_SERVICE_INFO,
|
||||
VICTRON_DC_ENERGY_METER_TOKEN,
|
||||
VICTRON_SOLAR_CHARGER_SERVICE_INFO,
|
||||
VICTRON_SOLAR_CHARGER_TOKEN,
|
||||
VICTRON_VEBUS_SERVICE_INFO,
|
||||
VICTRON_VEBUS_TOKEN,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.components.bluetooth import inject_bluetooth_service_info
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_bluetooth")
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"service_info",
|
||||
"access_token",
|
||||
),
|
||||
[
|
||||
(VICTRON_BATTERY_MONITOR_SERVICE_INFO, VICTRON_BATTERY_MONITOR_TOKEN),
|
||||
(VICTRON_DC_ENERGY_METER_SERVICE_INFO, VICTRON_DC_ENERGY_METER_TOKEN),
|
||||
(VICTRON_SOLAR_CHARGER_SERVICE_INFO, VICTRON_SOLAR_CHARGER_TOKEN),
|
||||
(VICTRON_VEBUS_SERVICE_INFO, VICTRON_VEBUS_TOKEN),
|
||||
],
|
||||
ids=["battery_monitor", "dc_energy_meter", "solar_charger", "vebus"],
|
||||
)
|
||||
async def test_sensors(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry_added_to_hass: MockConfigEntry,
|
||||
service_info: BluetoothServiceInfo,
|
||||
access_token: str,
|
||||
) -> None:
|
||||
"""Test sensor entities."""
|
||||
entry = mock_config_entry_added_to_hass
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Initially no entities should be created until bluetooth data is received
|
||||
assert len(hass.states.async_all()) == 0
|
||||
|
||||
# Inject bluetooth service info to trigger entity creation
|
||||
inject_bluetooth_service_info(hass, service_info)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Use snapshot testing to verify all entity states and registry entries
|
||||
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user