Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Franck Nijhof
ed4aa93ee8
Me, being optimistic <(*_*)> 2024-02-02 00:12:28 +01:00
Franck Nijhof
284fc94d31
Add initial label registry foundation
Add label support to device registry

Add label support to entity registry

Add support for calling services by labels

Rebase fixes, test fixes, linter fixes, catch up

Ran black

Update test snapshosts

Update snapshots for renault

Register API

Fix entity registry tests

Set up registry in bootstrap

Add labels to partial dict

Add support to Syrupy for test snapshotting

Adjust more tests

Adjust version in comment

Update snapshots

Add template methods
2024-02-01 23:35:13 +01:00
74 changed files with 6381 additions and 885 deletions

View file

@ -32,6 +32,7 @@ from .helpers import (
entity,
entity_registry,
issue_registry,
label_registry,
recorder,
restore_state,
template,
@ -298,6 +299,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
device_registry.async_load(hass),
entity_registry.async_load(hass),
issue_registry.async_load(hass),
label_registry.async_load(hass),
hass.async_add_executor_job(_cache_uname_processor),
template.async_load_custom_templates(hass),
restore_state.async_load(hass),

View file

@ -35,6 +35,7 @@ SECTIONS = (
"core",
"device_registry",
"entity_registry",
"label_registry",
"script",
"scene",
)

View file

@ -0,0 +1,128 @@
"""Websocket API to interact with the label registry."""
from typing import Any
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api.connection import ActiveConnection
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.label_registry import LabelEntry, async_get
async def async_setup(hass: HomeAssistant) -> bool:
"""Register the Label Registry WS commands."""
websocket_api.async_register_command(hass, websocket_list_labels)
websocket_api.async_register_command(hass, websocket_create_label)
websocket_api.async_register_command(hass, websocket_delete_label)
websocket_api.async_register_command(hass, websocket_update_label)
return True
@websocket_api.websocket_command(
{
vol.Required("type"): "config/label_registry/list",
}
)
@callback
def websocket_list_labels(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle list labels command."""
registry = async_get(hass)
connection.send_result(
msg["id"],
[_entry_dict(entry) for entry in registry.async_list_labels()],
)
@websocket_api.websocket_command(
{
vol.Required("type"): "config/label_registry/create",
vol.Required("name"): str,
vol.Optional("color"): vol.Any(str, None),
vol.Optional("description"): vol.Any(str, None),
vol.Optional("icon"): vol.Any(str, None),
}
)
@websocket_api.require_admin
@callback
def websocket_create_label(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Create label command."""
registry = async_get(hass)
data = dict(msg)
data.pop("type")
data.pop("id")
try:
entry = registry.async_create(**data)
except ValueError as err:
connection.send_error(msg["id"], "invalid_info", str(err))
else:
connection.send_result(msg["id"], _entry_dict(entry))
@websocket_api.websocket_command(
{
vol.Required("type"): "config/label_registry/delete",
vol.Required("label_id"): str,
}
)
@websocket_api.require_admin
@callback
def websocket_delete_label(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Delete label command."""
registry = async_get(hass)
try:
registry.async_delete(msg["label_id"])
except KeyError:
connection.send_error(msg["id"], "invalid_info", "Label ID doesn't exist")
else:
connection.send_message(websocket_api.result_message(msg["id"], "success"))
@websocket_api.websocket_command(
{
vol.Required("type"): "config/label_registry/update",
vol.Required("label_id"): str,
vol.Optional("color"): vol.Any(str, None),
vol.Optional("description"): vol.Any(str, None),
vol.Optional("icon"): vol.Any(str, None),
vol.Optional("name"): str,
}
)
@websocket_api.require_admin
@callback
def websocket_update_label(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle update label websocket command."""
registry = async_get(hass)
data = dict(msg)
data.pop("type")
data.pop("id")
try:
entry = registry.async_update(**data)
except ValueError as err:
connection.send_error(msg["id"], "invalid_info", str(err))
else:
connection.send_result(msg["id"], _entry_dict(entry))
@callback
def _entry_dict(entry: LabelEntry) -> dict[str, Any]:
"""Convert entry to API format."""
return {
"color": entry.color,
"description": entry.description,
"icon": entry.icon,
"label_id": entry.label_id,
"name": entry.name,
}

View file

@ -509,6 +509,10 @@ ATTR_AREA_ID: Final = "area_id"
# Contains one string, the device ID
ATTR_DEVICE_ID: Final = "device_id"
# Label identifier. Also used as service calls target parameter in which case
# it contains one string or a list of strings, each being an label id.
ATTR_LABEL_ID: Final = "label_id"
# String with a friendly name for the entity
ATTR_FRIENDLY_NAME: Final = "friendly_name"

View file

@ -29,6 +29,7 @@ from homeassistant.const import (
ATTR_AREA_ID,
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
ATTR_LABEL_ID,
CONF_ABOVE,
CONF_ALIAS,
CONF_ATTRIBUTE,
@ -1229,6 +1230,9 @@ ENTITY_SERVICE_FIELDS = {
vol.Optional(ATTR_AREA_ID): vol.Any(
ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
),
vol.Optional(ATTR_LABEL_ID): vol.Any(
ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
),
}
TARGET_SERVICE_FIELDS = {
@ -1246,6 +1250,9 @@ TARGET_SERVICE_FIELDS = {
vol.Optional(ATTR_AREA_ID): vol.Any(
ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
),
vol.Optional(ATTR_LABEL_ID): vol.Any(
ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)])
),
}

View file

@ -43,7 +43,7 @@ DATA_REGISTRY = "device_registry"
EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated"
STORAGE_KEY = "core.device_registry"
STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 4
STORAGE_VERSION_MINOR = 5
SAVE_DELAY = 10
CLEANUP_DELAY = 10
@ -238,6 +238,7 @@ class DeviceEntry:
hw_version: str | None = attr.ib(default=None)
id: str = attr.ib(factory=uuid_util.random_uuid_hex)
identifiers: set[tuple[str, str]] = attr.ib(converter=set, factory=set)
labels: set[str] = attr.ib(converter=set, factory=set)
manufacturer: str | None = attr.ib(default=None)
model: str | None = attr.ib(default=None)
name_by_user: str | None = attr.ib(default=None)
@ -378,6 +379,10 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
# Introduced in 2023.11
for device in old_data["devices"]:
device["serial_number"] = None
if old_minor_version < 5:
# Introduced in 2024.3
for device in old_data["devices"]:
device["labels"] = device.get("labels", [])
if old_major_version > 1:
raise NotImplementedError
@ -634,6 +639,7 @@ class DeviceRegistry:
disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED,
entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED,
hw_version: str | None | UndefinedType = UNDEFINED,
labels: set[str] | UndefinedType = UNDEFINED,
manufacturer: str | None | UndefinedType = UNDEFINED,
merge_connections: set[tuple[str, str]] | UndefinedType = UNDEFINED,
merge_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED,
@ -728,11 +734,12 @@ class DeviceRegistry:
("disabled_by", disabled_by),
("entry_type", entry_type),
("hw_version", hw_version),
("labels", labels),
("manufacturer", manufacturer),
("model", model),
("name", name),
("name_by_user", name_by_user),
("serial_number", serial_number),
("name", name),
("suggested_area", suggested_area),
("sw_version", sw_version),
("via_device_id", via_device_id),
@ -822,6 +829,7 @@ class DeviceRegistry:
tuple(iden) # type: ignore[misc]
for iden in device["identifiers"]
},
labels=set(device["labels"]),
manufacturer=device["manufacturer"],
model=device["model"],
name_by_user=device["name_by_user"],
@ -865,6 +873,7 @@ class DeviceRegistry:
"hw_version": entry.hw_version,
"id": entry.id,
"identifiers": list(entry.identifiers),
"labels": list(entry.labels),
"manufacturer": entry.manufacturer,
"model": entry.model,
"name_by_user": entry.name_by_user,
@ -937,6 +946,15 @@ class DeviceRegistry:
if area_id == device.area_id:
self.async_update_device(dev_id, area_id=None)
@callback
def async_clear_label_id(self, label_id: str) -> None:
"""Clear label from registry entries."""
for device_id, entry in self.devices.items():
if label_id in entry.labels:
labels = entry.labels.copy()
labels.remove(label_id)
self.async_update_device(device_id, labels=labels)
@callback
def async_get(hass: HomeAssistant) -> DeviceRegistry:
@ -957,6 +975,14 @@ def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> list[Devic
return [device for device in registry.devices.values() if device.area_id == area_id]
@callback
def async_entries_for_label(
registry: DeviceRegistry, label_id: str
) -> list[DeviceEntry]:
"""Return entries that match an label."""
return [device for device in registry.devices.values() if label_id in device.labels]
@callback
def async_entries_for_config_entry(
registry: DeviceRegistry, config_entry_id: str

View file

@ -65,7 +65,7 @@ SAVE_DELAY = 10
_LOGGER = logging.getLogger(__name__)
STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 12
STORAGE_VERSION_MINOR = 13
STORAGE_KEY = "core.entity_registry"
CLEANUP_INTERVAL = 3600 * 24
@ -135,6 +135,7 @@ ReadOnlyEntityOptionsType = ReadOnlyDict[str, ReadOnlyDict[str, Any]]
DISPLAY_DICT_OPTIONAL = (
("ai", "area_id"),
("lb", "labels"),
("di", "device_id"),
("ic", "icon"),
("tk", "translation_key"),
@ -174,6 +175,7 @@ class RegistryEntry:
converter=attr.converters.default_if_none(factory=uuid_util.random_uuid_hex), # type: ignore[misc]
)
has_entity_name: bool = attr.ib(default=False)
labels: set[str] = attr.ib(factory=set)
name: str | None = attr.ib(default=None)
options: ReadOnlyEntityOptionsType = attr.ib(
default=None, converter=_protect_entity_options
@ -268,6 +270,7 @@ class RegistryEntry:
"platform": self.platform,
"translation_key": self.translation_key,
"unique_id": self.unique_id,
"labels": self.labels,
}
@cached_property
@ -348,7 +351,7 @@ class DeletedRegistryEntry:
class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
"""Store entity registry data."""
async def _async_migrate_func(
async def _async_migrate_func( # noqa: C901
self,
old_major_version: int,
old_minor_version: int,
@ -429,6 +432,11 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
for entity in data["entities"]:
entity["previous_unique_id"] = None
if old_major_version == 1 and old_minor_version < 13:
# Version 1.13 adds labels
for entity in data["entities"]:
entity["labels"] = []
if old_major_version > 1:
raise NotImplementedError
return data
@ -853,9 +861,10 @@ class EntityRegistry:
device_id: str | None | UndefinedType = UNDEFINED,
disabled_by: RegistryEntryDisabler | None | UndefinedType = UNDEFINED,
entity_category: EntityCategory | None | UndefinedType = UNDEFINED,
has_entity_name: bool | UndefinedType = UNDEFINED,
hidden_by: RegistryEntryHider | None | UndefinedType = UNDEFINED,
icon: str | None | UndefinedType = UNDEFINED,
has_entity_name: bool | UndefinedType = UNDEFINED,
labels: set[str] | UndefinedType = UNDEFINED,
name: str | None | UndefinedType = UNDEFINED,
new_entity_id: str | UndefinedType = UNDEFINED,
new_unique_id: str | UndefinedType = UNDEFINED,
@ -903,9 +912,10 @@ class EntityRegistry:
("device_id", device_id),
("disabled_by", disabled_by),
("entity_category", entity_category),
("has_entity_name", has_entity_name),
("hidden_by", hidden_by),
("icon", icon),
("has_entity_name", has_entity_name),
("labels", labels),
("name", name),
("options", options),
("original_device_class", original_device_class),
@ -980,9 +990,10 @@ class EntityRegistry:
device_id: str | None | UndefinedType = UNDEFINED,
disabled_by: RegistryEntryDisabler | None | UndefinedType = UNDEFINED,
entity_category: EntityCategory | None | UndefinedType = UNDEFINED,
has_entity_name: bool | UndefinedType = UNDEFINED,
hidden_by: RegistryEntryHider | None | UndefinedType = UNDEFINED,
icon: str | None | UndefinedType = UNDEFINED,
has_entity_name: bool | UndefinedType = UNDEFINED,
labels: set[str] | UndefinedType = UNDEFINED,
name: str | None | UndefinedType = UNDEFINED,
new_entity_id: str | UndefinedType = UNDEFINED,
new_unique_id: str | UndefinedType = UNDEFINED,
@ -1004,9 +1015,10 @@ class EntityRegistry:
device_id=device_id,
disabled_by=disabled_by,
entity_category=entity_category,
has_entity_name=has_entity_name,
hidden_by=hidden_by,
icon=icon,
has_entity_name=has_entity_name,
labels=labels,
name=name,
new_entity_id=new_entity_id,
new_unique_id=new_unique_id,
@ -1098,12 +1110,13 @@ class EntityRegistry:
if entity["entity_category"]
else None,
entity_id=entity["entity_id"],
has_entity_name=entity["has_entity_name"],
hidden_by=RegistryEntryHider(entity["hidden_by"])
if entity["hidden_by"]
else None,
icon=entity["icon"],
id=entity["id"],
has_entity_name=entity["has_entity_name"],
labels=set(entity["labels"]),
name=entity["name"],
options=entity["options"],
original_device_class=entity["original_device_class"],
@ -1156,10 +1169,11 @@ class EntityRegistry:
"disabled_by": entry.disabled_by,
"entity_category": entry.entity_category,
"entity_id": entry.entity_id,
"has_entity_name": entry.has_entity_name,
"hidden_by": entry.hidden_by,
"icon": entry.icon,
"id": entry.id,
"has_entity_name": entry.has_entity_name,
"labels": list(entry.labels),
"name": entry.name,
"options": entry.options,
"original_device_class": entry.original_device_class,
@ -1230,6 +1244,15 @@ class EntityRegistry:
if area_id == entry.area_id:
self.async_update_entity(entity_id, area_id=None)
@callback
def async_clear_label_id(self, label_id: str) -> None:
"""Clear label from registry entries."""
for entity_id, entry in self.entities.items():
if label_id in entry.labels:
labels = entry.labels.copy()
labels.remove(label_id)
self.async_update_entity(entity_id, labels=labels)
@callback
def async_get(hass: HomeAssistant) -> EntityRegistry:
@ -1264,6 +1287,14 @@ def async_entries_for_area(
return [entry for entry in registry.entities.values() if entry.area_id == area_id]
@callback
def async_entries_for_label(
registry: EntityRegistry, label_id: str
) -> list[RegistryEntry]:
"""Return entries that match an label."""
return [entry for entry in registry.entities.values() if label_id in entry.labels]
@callback
def async_entries_for_config_entry(
registry: EntityRegistry, config_entry_id: str

View file

@ -0,0 +1,242 @@
"""Provide a way to label and categorize anything."""
from __future__ import annotations
from collections.abc import Iterable, MutableMapping
import dataclasses
from dataclasses import dataclass
from typing import cast
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import slugify
from . import device_registry as dr, entity_registry as er
from .typing import UNDEFINED, UndefinedType
DATA_REGISTRY = "label_registry"
EVENT_LABEL_REGISTRY_UPDATED = "label_registry_updated"
STORAGE_KEY = "core.label_registry"
STORAGE_VERSION_MAJOR = 1
SAVE_DELAY = 10
@dataclass(slots=True, frozen=True)
class LabelEntry:
"""Label Registry Entry."""
label_id: str
name: str
normalized_name: str
description: str | None = None
color: str | None = None
icon: str | None = None
class LabelRegistry:
"""Class to hold a registry of labels."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the label registry."""
self.hass = hass
self.labels: MutableMapping[str, LabelEntry] = {}
self._store = hass.helpers.storage.Store(
STORAGE_VERSION_MAJOR,
STORAGE_KEY,
atomic_writes=True,
)
self._normalized_name_label_idx: dict[str, str] = {}
self.children: dict[str, set[str]] = {}
@callback
def async_get_label(self, label_id: str) -> LabelEntry | None:
"""Get label by id."""
return self.labels.get(label_id)
@callback
def async_get_label_by_name(self, name: str) -> LabelEntry | None:
"""Get label by name."""
normalized_name = normalize_label_name(name)
if normalized_name not in self._normalized_name_label_idx:
return None
return self.labels[self._normalized_name_label_idx[normalized_name]]
@callback
def async_list_labels(self) -> Iterable[LabelEntry]:
"""Get all labels."""
return self.labels.values()
@callback
def async_get_or_create(self, name: str) -> LabelEntry:
"""Get or create an label."""
if label := self.async_get_label_by_name(name):
return label
return self.async_create(name)
@callback
def _generate_id(self, name: str) -> str:
"""Initialize ID."""
suggestion = suggestion_base = slugify(name)
tries = 1
while suggestion in self.labels:
tries += 1
suggestion = f"{suggestion_base}_{tries}"
return suggestion
@callback
def async_create(
self,
name: str,
*,
color: str | None = None,
icon: str | None = None,
description: str | None = None,
) -> LabelEntry:
"""Create a new label."""
normalized_name = normalize_label_name(name)
if self.async_get_label_by_name(name):
raise ValueError(f"The name {name} ({normalized_name}) is already in use")
label = LabelEntry(
color=color,
description=description,
icon=icon,
label_id=self._generate_id(name),
name=name,
normalized_name=normalized_name,
)
self.labels[label.label_id] = label
self._normalized_name_label_idx[normalized_name] = label.label_id
self.async_schedule_save()
self.hass.bus.async_fire(
EVENT_LABEL_REGISTRY_UPDATED,
{"action": "create", "label_id": label.label_id},
)
return label
@callback
def async_delete(self, label_id: str) -> None:
"""Delete label."""
label = self.labels[label_id]
# Clean up all references
dr.async_get(self.hass).async_clear_label_id(label_id)
er.async_get(self.hass).async_clear_label_id(label_id)
del self.labels[label_id]
del self._normalized_name_label_idx[label.normalized_name]
self.hass.bus.async_fire(
EVENT_LABEL_REGISTRY_UPDATED, {"action": "remove", "label_id": label_id}
)
self.async_schedule_save()
@callback
def async_update(
self,
label_id: str,
color: str | None | UndefinedType = UNDEFINED,
description: str | None | UndefinedType = UNDEFINED,
icon: str | None | UndefinedType = UNDEFINED,
name: str | UndefinedType = UNDEFINED,
) -> LabelEntry:
"""Update name of label."""
old = self.labels[label_id]
changes = {
attr_name: value
for attr_name, value in (
("color", color),
("description", description),
("icon", icon),
)
if value is not UNDEFINED and getattr(old, attr_name) != value
}
normalized_name = None
if name is not UNDEFINED and name != old.name:
normalized_name = normalize_label_name(name)
if normalized_name != old.normalized_name and self.async_get_label_by_name(
name
):
raise ValueError(
f"The name {name} ({normalized_name}) is already in use"
)
changes["name"] = name
changes["normalized_name"] = normalized_name
if not changes:
return old
new = self.labels[label_id] = dataclasses.replace(old, **changes) # type: ignore[arg-type]
if normalized_name is not None:
self._normalized_name_label_idx[
normalized_name
] = self._normalized_name_label_idx.pop(old.normalized_name)
self.async_schedule_save()
self.hass.bus.async_fire(
EVENT_LABEL_REGISTRY_UPDATED, {"action": "update", "label_id": label_id}
)
return new
async def async_load(self) -> None:
"""Load the label registry."""
data = await self._store.async_load()
labels: MutableMapping[str, LabelEntry] = {}
if data is not None:
for label in data["labels"]:
normalized_name = normalize_label_name(label["name"])
labels[label["label_id"]] = LabelEntry(
color=label["color"],
description=label["description"],
icon=label["icon"],
label_id=label["label_id"],
name=label["name"],
normalized_name=normalized_name,
)
self._normalized_name_label_idx[normalized_name] = label["label_id"]
self.labels = labels
@callback
def async_schedule_save(self) -> None:
"""Schedule saving the label registry."""
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
@callback
def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]:
"""Return data of label registry to store in a file."""
return {
"labels": [
{
"color": entry.color,
"description": entry.description,
"icon": entry.icon,
"label_id": entry.label_id,
"name": entry.name,
}
for entry in self.labels.values()
]
}
@callback
def async_get(hass: HomeAssistant) -> LabelRegistry:
"""Get label registry."""
return cast(LabelRegistry, hass.data[DATA_REGISTRY])
async def async_load(hass: HomeAssistant) -> None:
"""Load label registry."""
assert DATA_REGISTRY not in hass.data
hass.data[DATA_REGISTRY] = LabelRegistry(hass)
await hass.data[DATA_REGISTRY].async_load()
def normalize_label_name(label_name: str) -> str:
"""Normalize an label name by removing whitespace and case folding."""
return label_name.casefold().replace(" ", "")

View file

@ -17,6 +17,7 @@ from homeassistant.const import (
ATTR_AREA_ID,
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
ATTR_LABEL_ID,
CONF_ENTITY_ID,
CONF_SERVICE,
CONF_SERVICE_DATA,
@ -51,6 +52,7 @@ from . import (
config_validation as cv,
device_registry,
entity_registry,
label_registry,
template,
translation,
)
@ -200,6 +202,7 @@ class ServiceTargetSelector:
entity_ids: str | list | None = service_call_data.get(ATTR_ENTITY_ID)
device_ids: str | list | None = service_call_data.get(ATTR_DEVICE_ID)
area_ids: str | list | None = service_call_data.get(ATTR_AREA_ID)
label_ids: str | list | None = service_call_data.get(ATTR_LABEL_ID)
self.entity_ids = (
set(cv.ensure_list(entity_ids)) if _has_match(entity_ids) else set()
@ -208,11 +211,16 @@ class ServiceTargetSelector:
set(cv.ensure_list(device_ids)) if _has_match(device_ids) else set()
)
self.area_ids = set(cv.ensure_list(area_ids)) if _has_match(area_ids) else set()
self.label_ids = (
set(cv.ensure_list(label_ids)) if _has_match(label_ids) else set()
)
@property
def has_any_selector(self) -> bool:
"""Determine if any selectors are present."""
return bool(self.entity_ids or self.device_ids or self.area_ids)
return bool(
self.entity_ids or self.device_ids or self.area_ids or self.label_ids
)
@dataclasses.dataclass(slots=True)
@ -222,16 +230,19 @@ class SelectedEntities:
# Entities that were explicitly mentioned.
referenced: set[str] = dataclasses.field(default_factory=set)
# Entities that were referenced via device/area ID.
# Entities that were referenced via device/area/label ID.
# Should not trigger a warning when they don't exist.
indirectly_referenced: set[str] = dataclasses.field(default_factory=set)
# Referenced items that could not be found.
missing_devices: set[str] = dataclasses.field(default_factory=set)
missing_areas: set[str] = dataclasses.field(default_factory=set)
missing_labels: set[str] = dataclasses.field(default_factory=set)
# Referenced devices
referenced_devices: set[str] = dataclasses.field(default_factory=set)
referenced_devices_by_labels: set[str] = dataclasses.field(default_factory=set)
referenced_devices_by_areas: set[str] = dataclasses.field(default_factory=set)
def log_missing(self, missing_entities: set[str]) -> None:
"""Log about missing items."""
@ -240,6 +251,7 @@ class SelectedEntities:
("areas", self.missing_areas),
("devices", self.missing_devices),
("entities", missing_entities),
("labels", self.missing_labels),
):
if items:
parts.append(f"{label} {', '.join(sorted(items))}")
@ -470,12 +482,13 @@ def async_extract_referenced_entity_ids(
selected.referenced.update(entity_ids)
if not selector.device_ids and not selector.area_ids:
if not selector.device_ids and not selector.area_ids and not selector.label_ids:
return selected
ent_reg = entity_registry.async_get(hass)
dev_reg = device_registry.async_get(hass)
area_reg = area_registry.async_get(hass)
label_reg = label_registry.async_get(hass)
for device_id in selector.device_ids:
if device_id not in dev_reg.devices:
@ -485,13 +498,29 @@ def async_extract_referenced_entity_ids(
if area_id not in area_reg.areas:
selected.missing_areas.add(area_id)
for label_id in selector.label_ids:
if label_id not in label_reg.labels:
selected.missing_labels.add(label_id)
# Find devices for targeted areas
selected.referenced_devices.update(selector.device_ids)
for device_entry in dev_reg.devices.values():
if device_entry.area_id in selector.area_ids:
selected.referenced_devices.add(device_entry.id)
selected.referenced_devices_by_areas.add(device_entry.id)
if not selector.area_ids and not selected.referenced_devices:
# Find devices for targeted labels
selected.referenced_devices.update(selector.device_ids)
for device_entry in dev_reg.devices.values():
if device_entry.labels.intersection(selector.label_ids):
selected.referenced_devices.add(device_entry.id)
selected.referenced_devices_by_labels.add(device_entry.id)
if (
not selector.area_ids
and not selector.label_ids
and not selected.referenced_devices
):
return selected
for ent_entry in ent_reg.entities.values():
@ -507,10 +536,14 @@ def async_extract_referenced_entity_ids(
# has no explicitly set area
or (
not ent_entry.area_id
and ent_entry.device_id in selected.referenced_devices
and ent_entry.device_id in selected.referenced_devices_by_areas
)
# The entity's device matches a targeted device
or ent_entry.device_id in selector.device_ids
# The entity's label matches a targeted label
or ent_entry.labels.intersection(selector.label_ids)
# The entity's device matches a device referenced by an label
or ent_entry.device_id in selected.referenced_devices_by_labels
):
selected.indirectly_referenced.add(ent_entry.entity_id)

View file

@ -78,7 +78,13 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
from homeassistant.util.read_only_dict import ReadOnlyDict
from homeassistant.util.thread import ThreadWithException
from . import area_registry, device_registry, entity_registry, location as loc_helper
from . import (
area_registry,
device_registry,
entity_registry,
label_registry,
location as loc_helper,
)
from .singleton import singleton
from .typing import TemplateVarsType
@ -1451,6 +1457,81 @@ def area_devices(hass: HomeAssistant, area_id_or_name: str) -> Iterable[str]:
return [entry.id for entry in entries]
def labels(hass: HomeAssistant, lookup_value: Any = None) -> Iterable[str | None]:
"""Return all labels, or those from a device ID or entity ID."""
label_reg = label_registry.async_get(hass)
if lookup_value is None:
return [label.label_id for label in label_reg.async_list_labels()]
ent_reg = entity_registry.async_get(hass)
# Import here, not at top-level to avoid circular import
from . import config_validation as cv # pylint: disable=import-outside-toplevel
try:
cv.entity_id(lookup_value)
except vol.Invalid:
pass
else:
if entity := ent_reg.async_get(str(lookup_value)):
return list(entity.labels)
# Check if this could be a device ID
dev_reg = device_registry.async_get(hass)
if device := dev_reg.async_get(str(lookup_value)):
return list(device.labels)
return []
def label_id(hass: HomeAssistant, lookup_value: Any) -> str | None:
"""Get the label ID from an label name."""
label_reg = label_registry.async_get(hass)
if label := label_reg.async_get_label_by_name(str(lookup_value)):
return label.label_id
return None
def label_name(hass: HomeAssistant, lookup_value: str) -> str | None:
"""Get the label name from an label id."""
label_reg = label_registry.async_get(hass)
if label := label_reg.async_get_label(lookup_value):
return label.name
return None
def label_devices(hass: HomeAssistant, label_id_or_name: str) -> Iterable[str]:
"""Return device IDs for a given label ID or name."""
_label_id: str | None
# if label_name returns a value, we know the input was an ID, otherwise we
# assume it's a name, and if it's neither, we return early
if label_name(hass, label_id_or_name) is not None:
_label_id = label_id_or_name
else:
_label_id = label_id(hass, label_id_or_name)
if _label_id is None:
return []
dev_reg = device_registry.async_get(hass)
entries = device_registry.async_entries_for_label(dev_reg, _label_id)
return [entry.id for entry in entries]
def label_entities(hass: HomeAssistant, label_id_or_name: str) -> Iterable[str]:
"""Return entities for a given label ID or name."""
_label_id: str | None
# if label_name returns a value, we know the input was an ID, otherwise we
# assume it's a name, and if it's neither, we return early
if label_name(hass, label_id_or_name) is not None:
_label_id = label_id_or_name
else:
_label_id = label_id(hass, label_id_or_name)
if _label_id is None:
return []
ent_reg = entity_registry.async_get(hass)
entries = entity_registry.async_entries_for_label(ent_reg, _label_id)
return [entry.entity_id for entry in entries]
def closest(hass, *args):
"""Find closest entity.
@ -2603,6 +2684,21 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["area_devices"] = hassfunction(area_devices)
self.filters["area_devices"] = self.globals["area_devices"]
self.globals["labels"] = hassfunction(labels)
self.filters["labels"] = self.globals["labels"]
self.globals["label_id"] = hassfunction(label_id)
self.filters["label_id"] = self.globals["label_id"]
self.globals["label_name"] = hassfunction(label_name)
self.filters["label_name"] = self.globals["label_name"]
self.globals["label_devices"] = hassfunction(label_devices)
self.filters["label_devices"] = self.globals["label_devices"]
self.globals["label_entities"] = hassfunction(label_entities)
self.filters["label_entities"] = self.globals["label_entities"]
self.globals["integration_entities"] = hassfunction(integration_entities)
self.filters["integration_entities"] = self.globals["integration_entities"]
@ -2636,6 +2732,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"area_name",
"relative_time",
"today_at",
"label_id",
"label_name",
]
hass_filters = [
"closest",
@ -2644,6 +2742,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"area_id",
"area_name",
"has_value",
"label_id",
"label_name",
]
hass_tests = [
"has_value",

View file

@ -688,6 +688,7 @@ voluptuous = "vol"
"homeassistant.helpers.device_registry" = "dr"
"homeassistant.helpers.entity_registry" = "er"
"homeassistant.helpers.issue_registry" = "ir"
"homeassistant.helpers.label_registry" = "lr"
"homeassistant.util.dt" = "dt_util"
[tool.ruff.flake8-pytest-style]

View file

@ -65,6 +65,7 @@ from homeassistant.helpers import (
event,
intent,
issue_registry as ir,
label_registry as lr,
recorder as recorder_helper,
restore_state,
restore_state as rs,
@ -281,6 +282,7 @@ async def async_test_home_assistant(event_loop, load_registries=True):
dr.async_load(hass),
er.async_load(hass),
ir.async_load(hass),
lr.async_load(hass),
rs.async_load(hass),
)
hass.data[bootstrap.DATA_REGISTRIES_LOADED] = None

View file

@ -74,6 +74,7 @@ async def test_list_entities(hass: HomeAssistant, client) -> None:
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"name": "Hello World",
"options": {},
"original_name": None,
@ -92,6 +93,7 @@ async def test_list_entities(hass: HomeAssistant, client) -> None:
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"name": None,
"options": {},
"original_name": None,
@ -137,6 +139,7 @@ async def test_list_entities(hass: HomeAssistant, client) -> None:
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"name": "Hello World",
"options": {},
"original_name": None,
@ -234,6 +237,7 @@ async def test_list_entities_for_display(
"ei": "test_domain.test",
"en": "Hello World",
"ic": "mdi:icon",
"lb": [],
"pl": "test_platform",
"tk": "translations_galore",
},
@ -242,31 +246,37 @@ async def test_list_entities_for_display(
"di": "device123",
"ei": "test_domain.nameless",
"en": None,
"lb": [],
"pl": "test_platform",
},
{
"ai": "area52",
"di": "device123",
"ei": "test_domain.renamed",
"lb": [],
"pl": "test_platform",
},
{
"ei": "test_domain.boring",
"lb": [],
"pl": "test_platform",
},
{
"ei": "test_domain.hidden",
"lb": [],
"hb": True,
"pl": "test_platform",
},
{
"dp": 0,
"ei": "sensor.default_precision",
"lb": [],
"pl": "test_platform",
},
{
"dp": 0,
"ei": "sensor.user_precision",
"lb": [],
"pl": "test_platform",
},
],
@ -308,6 +318,7 @@ async def test_list_entities_for_display(
"di": "device123",
"ei": "test_domain.test",
"en": "Hello World",
"lb": [],
"pl": "test_platform",
},
],
@ -352,6 +363,7 @@ async def test_get_entity(hass: HomeAssistant, client) -> None:
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"name": "Hello World",
"options": {},
"original_device_class": None,
@ -385,6 +397,7 @@ async def test_get_entity(hass: HomeAssistant, client) -> None:
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"name": None,
"options": {},
"original_device_class": None,
@ -443,6 +456,7 @@ async def test_get_entities(hass: HomeAssistant, client) -> None:
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"name": "Hello World",
"options": {},
"original_device_class": None,
@ -466,6 +480,7 @@ async def test_get_entities(hass: HomeAssistant, client) -> None:
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"name": None,
"options": {},
"original_device_class": None,
@ -535,6 +550,7 @@ async def test_update_entity(hass: HomeAssistant, client) -> None:
"hidden_by": "user", # We exchange strings over the WS API, not enums
"icon": "icon:after update",
"id": ANY,
"labels": [],
"name": "after update",
"options": {},
"original_device_class": None,
@ -610,6 +626,7 @@ async def test_update_entity(hass: HomeAssistant, client) -> None:
"hidden_by": "user", # We exchange strings over the WS API, not enums
"icon": "icon:after update",
"id": ANY,
"labels": [],
"name": "after update",
"options": {},
"original_device_class": None,
@ -650,6 +667,7 @@ async def test_update_entity(hass: HomeAssistant, client) -> None:
"hidden_by": "user", # We exchange strings over the WS API, not enums
"icon": "icon:after update",
"id": ANY,
"labels": [],
"name": "after update",
"options": {"sensor": {"unit_of_measurement": "beard_second"}},
"original_device_class": None,
@ -702,6 +720,7 @@ async def test_update_entity_require_restart(hass: HomeAssistant, client) -> Non
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"name": None,
"options": {},
"original_device_class": None,
@ -812,6 +831,7 @@ async def test_update_entity_no_changes(hass: HomeAssistant, client) -> None:
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"name": "name of entity",
"options": {},
"original_device_class": None,
@ -901,6 +921,7 @@ async def test_update_entity_id(hass: HomeAssistant, client) -> None:
"hidden_by": None,
"icon": None,
"id": ANY,
"labels": [],
"name": None,
"options": {},
"original_device_class": None,

View file

@ -0,0 +1,251 @@
"""Test label registry API."""
from collections.abc import Awaitable, Callable, Generator
from typing import Any
from aiohttp import ClientWebSocketResponse
import pytest
from homeassistant.components.config import label_registry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import label_registry as lr
@pytest.fixture(name="client")
def client_fixture(
hass: HomeAssistant,
hass_ws_client: Callable[
[HomeAssistant], Awaitable[Generator[ClientWebSocketResponse, Any, Any]]
],
) -> Generator[ClientWebSocketResponse, None, None]:
"""Fixture that can interact with the config manager API."""
hass.loop.run_until_complete(label_registry.async_setup(hass))
return hass.loop.run_until_complete(hass_ws_client(hass))
async def test_list_labels(
hass: HomeAssistant,
client: ClientWebSocketResponse,
label_registry: lr.LabelRegistry,
) -> None:
"""Test list entries."""
label_registry.async_create("mock 1")
label_registry.async_create(
name="mock 2",
color="#00FF00",
icon="mdi:two",
description="This is the second label",
)
assert len(label_registry.labels) == 2
await client.send_json({"id": 1, "type": "config/label_registry/list"})
msg = await client.receive_json()
assert len(msg["result"]) == len(label_registry.labels)
assert msg["result"][0] == {
"color": None,
"description": None,
"icon": None,
"label_id": "mock_1",
"name": "mock 1",
}
assert msg["result"][1] == {
"color": "#00FF00",
"description": "This is the second label",
"icon": "mdi:two",
"label_id": "mock_2",
"name": "mock 2",
}
async def test_create_label(
hass: HomeAssistant,
client: ClientWebSocketResponse,
label_registry: lr.LabelRegistry,
) -> None:
"""Test create entry."""
await client.send_json(
{
"id": 1,
"name": "MOCK",
"type": "config/label_registry/create",
}
)
msg = await client.receive_json()
assert len(label_registry.labels) == 1
assert msg["result"] == {
"color": None,
"description": None,
"icon": None,
"label_id": "mock",
"name": "MOCK",
}
await client.send_json(
{
"id": 2,
"name": "MOCKERY",
"type": "config/label_registry/create",
"color": "#00FF00",
"description": "This is the second label",
"icon": "mdi:two",
}
)
msg = await client.receive_json()
assert len(label_registry.labels) == 2
assert msg["result"] == {
"color": "#00FF00",
"description": "This is the second label",
"icon": "mdi:two",
"label_id": "mockery",
"name": "MOCKERY",
}
async def test_create_label_with_name_already_in_use(
hass: HomeAssistant,
client: ClientWebSocketResponse,
label_registry: lr.LabelRegistry,
) -> None:
"""Test create entry that should fail."""
label_registry.async_create("mock")
assert len(label_registry.labels) == 1
await client.send_json(
{"id": 1, "name": "mock", "type": "config/label_registry/create"}
)
msg = await client.receive_json()
assert not msg["success"]
assert msg["error"]["code"] == "invalid_info"
assert msg["error"]["message"] == "The name mock (mock) is already in use"
assert len(label_registry.labels) == 1
async def test_delete_label(
hass: HomeAssistant,
client: ClientWebSocketResponse,
label_registry: lr.LabelRegistry,
) -> None:
"""Test delete entry."""
label = label_registry.async_create("mock")
assert len(label_registry.labels) == 1
await client.send_json(
{"id": 1, "label_id": label.label_id, "type": "config/label_registry/delete"}
)
msg = await client.receive_json()
assert msg["success"]
assert not label_registry.labels
async def test_delete_non_existing_label(
hass: HomeAssistant,
client: ClientWebSocketResponse,
label_registry: lr.LabelRegistry,
) -> None:
"""Test delete entry that should fail."""
label_registry.async_create("mock")
assert len(label_registry.labels) == 1
await client.send_json(
{"id": 1, "label_id": "omg_puppies", "type": "config/label_registry/delete"}
)
msg = await client.receive_json()
assert not msg["success"]
assert msg["error"]["code"] == "invalid_info"
assert msg["error"]["message"] == "Label ID doesn't exist"
assert len(label_registry.labels) == 1
async def test_update_label(
hass: HomeAssistant,
client: ClientWebSocketResponse,
label_registry: lr.LabelRegistry,
) -> None:
"""Test update entry."""
label = label_registry.async_create("mock")
assert len(label_registry.labels) == 1
await client.send_json(
{
"id": 1,
"label_id": label.label_id,
"name": "UPDATED",
"icon": "mdi:test",
"color": "#00FF00",
"description": "This is an label description",
"type": "config/label_registry/update",
}
)
msg = await client.receive_json()
assert len(label_registry.labels) == 1
assert msg["result"] == {
"color": "#00FF00",
"description": "This is an label description",
"icon": "mdi:test",
"label_id": "mock",
"name": "UPDATED",
}
await client.send_json(
{
"id": 2,
"label_id": label.label_id,
"name": "UPDATED AGAIN",
"icon": None,
"color": None,
"description": None,
"type": "config/label_registry/update",
}
)
msg = await client.receive_json()
assert len(label_registry.labels) == 1
assert msg["result"] == {
"color": None,
"description": None,
"icon": None,
"label_id": "mock",
"name": "UPDATED AGAIN",
}
async def test_update_with_name_already_in_use(
hass: HomeAssistant,
client: ClientWebSocketResponse,
label_registry: lr.LabelRegistry,
) -> None:
"""Test update entry."""
label = label_registry.async_create("mock 1")
label_registry.async_create("mock 2")
assert len(label_registry.labels) == 2
await client.send_json(
{
"id": 1,
"label_id": label.label_id,
"name": "mock 2",
"type": "config/label_registry/update",
}
)
msg = await client.receive_json()
assert not msg["success"]
assert msg["error"]["code"] == "invalid_info"
assert msg["error"]["message"] == "The name mock 2 (mock2) is already in use"
assert len(label_registry.labels) == 2

View file

@ -29,6 +29,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -73,6 +75,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -116,6 +120,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -44,6 +44,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -31,6 +31,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -38,6 +38,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -91,6 +93,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -33,6 +33,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -81,6 +83,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -129,6 +133,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -177,6 +183,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -36,6 +36,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -87,6 +89,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -138,6 +142,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -28,6 +28,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -30,6 +30,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -72,6 +72,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -159,6 +161,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -246,6 +250,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -333,6 +339,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -16,6 +16,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -21,6 +21,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'devolo',
'model': 'dLAN pro 1200+ WiFi ac',
'name': 'Mock Title',

View file

@ -29,6 +29,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -76,6 +78,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -120,6 +124,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -115,6 +115,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -159,6 +161,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -39,6 +39,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -29,6 +29,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -65,6 +67,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light Mini',
'name': 'Frenck',
@ -105,6 +109,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -141,6 +147,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light Mini',
'name': 'Frenck',

View file

@ -61,6 +61,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -97,6 +99,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light',
'name': 'Frenck',
@ -171,6 +175,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -207,6 +213,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light',
'name': 'Frenck',
@ -281,6 +289,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -317,6 +327,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light',
'name': 'Frenck',

View file

@ -33,6 +33,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
@ -72,6 +74,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light Mini',
'name': 'Frenck',
@ -116,6 +120,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
@ -158,6 +164,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light Mini',
'name': 'Frenck',
@ -202,6 +210,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
@ -244,6 +254,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light Mini',
'name': 'Frenck',
@ -288,6 +300,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
@ -327,6 +341,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light Mini',
'name': 'Frenck',
@ -371,6 +387,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
@ -413,6 +431,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light Mini',
'name': 'Frenck',

View file

@ -29,6 +29,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -65,6 +67,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light Mini',
'name': 'Frenck',
@ -105,6 +109,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -141,6 +147,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light Mini',
'name': 'Frenck',

View file

@ -487,6 +487,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -512,6 +514,8 @@
'hw_version': None,
'id': <ANY>,
'is_new': False,
'labels': set({
}),
'manufacturer': 'EnergyZero',
'model': None,
'name': 'Energy market price',
@ -556,6 +560,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -581,6 +587,8 @@
'hw_version': None,
'id': <ANY>,
'is_new': False,
'labels': set({
}),
'manufacturer': 'EnergyZero',
'model': None,
'name': 'Energy market price',
@ -622,6 +630,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -647,6 +657,8 @@
'hw_version': None,
'id': <ANY>,
'is_new': False,
'labels': set({
}),
'manufacturer': 'EnergyZero',
'model': None,
'name': 'Energy market price',
@ -689,6 +701,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -714,6 +728,8 @@
'hw_version': None,
'id': <ANY>,
'is_new': False,
'labels': set({
}),
'manufacturer': 'EnergyZero',
'model': None,
'name': 'Energy market price',
@ -755,6 +771,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -780,6 +798,8 @@
'hw_version': None,
'id': <ANY>,
'is_new': False,
'labels': set({
}),
'manufacturer': 'EnergyZero',
'model': None,
'name': 'Energy market price',
@ -824,6 +844,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -849,6 +871,8 @@
'hw_version': None,
'id': <ANY>,
'is_new': False,
'labels': set({
}),
'manufacturer': 'EnergyZero',
'model': None,
'name': 'Gas market price',

View file

@ -17,6 +17,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': 'Mock Model',
'name': 'Mock Title',

View file

@ -102,6 +102,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -78,6 +78,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -107,6 +109,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -136,6 +140,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -165,6 +171,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -194,6 +202,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

File diff suppressed because it is too large Load diff

View file

@ -29,6 +29,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -65,6 +67,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'HWE-P1',
'name': 'Device',

View file

@ -38,6 +38,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -74,6 +76,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'HWE-SKT',
'name': 'Device',

File diff suppressed because it is too large Load diff

View file

@ -179,6 +179,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -215,6 +217,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'HWE-SKT',
'name': 'Device',
@ -254,6 +258,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -290,6 +296,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'HWE-SKT',
'name': 'Device',
@ -329,6 +337,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -365,6 +375,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'HWE-SKT',
'name': 'Device',

View file

@ -30,6 +30,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2405',
'name': '05.111111111111',
@ -68,6 +70,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18S20',
'name': '10.111111111111',
@ -106,6 +110,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2406',
'name': '12.111111111111',
@ -135,6 +141,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -164,6 +172,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -226,6 +236,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2423',
'name': '1D.111111111111',
@ -264,6 +276,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2409',
'name': '1F.111111111111',
@ -290,6 +304,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2423',
'name': '1D.111111111111',
@ -328,6 +344,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS1822',
'name': '22.111111111111',
@ -366,6 +384,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2438',
'name': '26.111111111111',
@ -404,6 +424,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18B20',
'name': '28.111111111111',
@ -442,6 +464,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18B20',
'name': '28.222222222222',
@ -480,6 +504,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18B20',
'name': '28.222222222223',
@ -518,6 +544,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2408',
'name': '29.111111111111',
@ -547,6 +575,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -576,6 +606,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -605,6 +637,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -634,6 +668,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -663,6 +699,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -692,6 +730,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -721,6 +761,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -750,6 +792,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -884,6 +928,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2760',
'name': '30.111111111111',
@ -922,6 +968,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2413',
'name': '3A.111111111111',
@ -951,6 +999,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -980,6 +1030,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1042,6 +1094,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS1825',
'name': '3B.111111111111',
@ -1080,6 +1134,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS28EA00',
'name': '42.111111111111',
@ -1118,6 +1174,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Embedded Data Systems',
'model': 'EDS0068',
'name': '7E.111111111111',
@ -1156,6 +1214,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Embedded Data Systems',
'model': 'EDS0066',
'name': '7E.222222222222',
@ -1194,6 +1254,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Hobby Boards',
'model': 'HobbyBoards_EF',
'name': 'EF.111111111111',
@ -1232,6 +1294,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Hobby Boards',
'model': 'HB_MOISTURE_METER',
'name': 'EF.111111111112',
@ -1270,6 +1334,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Hobby Boards',
'model': 'HB_HUB',
'name': 'EF.111111111113',
@ -1299,6 +1365,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1328,6 +1396,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1357,6 +1427,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1386,6 +1458,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -30,6 +30,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2405',
'name': '05.111111111111',
@ -68,6 +70,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18S20',
'name': '10.111111111111',
@ -99,6 +103,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -152,6 +158,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2406',
'name': '12.111111111111',
@ -183,6 +191,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -214,6 +224,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -282,6 +294,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2423',
'name': '1D.111111111111',
@ -313,6 +327,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -344,6 +360,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -410,6 +428,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2409',
'name': '1F.111111111111',
@ -436,6 +456,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2423',
'name': '1D.111111111111',
@ -467,6 +489,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -498,6 +522,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -564,6 +590,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS1822',
'name': '22.111111111111',
@ -595,6 +623,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -648,6 +678,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2438',
'name': '26.111111111111',
@ -679,6 +711,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -710,6 +744,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -741,6 +777,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -772,6 +810,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -803,6 +843,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -834,6 +876,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -865,6 +909,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -896,6 +942,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -927,6 +975,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -958,6 +1008,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -989,6 +1041,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1192,6 +1246,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18B20',
'name': '28.111111111111',
@ -1223,6 +1279,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1276,6 +1334,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18B20',
'name': '28.222222222222',
@ -1307,6 +1367,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1360,6 +1422,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18B20',
'name': '28.222222222223',
@ -1391,6 +1455,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1444,6 +1510,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2408',
'name': '29.111111111111',
@ -1482,6 +1550,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2760',
'name': '30.111111111111',
@ -1513,6 +1583,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1544,6 +1616,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1575,6 +1649,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1606,6 +1682,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1704,6 +1782,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2413',
'name': '3A.111111111111',
@ -1742,6 +1822,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS1825',
'name': '3B.111111111111',
@ -1773,6 +1855,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1826,6 +1910,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS28EA00',
'name': '42.111111111111',
@ -1857,6 +1943,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1910,6 +1998,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Embedded Data Systems',
'model': 'EDS0068',
'name': '7E.111111111111',
@ -1941,6 +2031,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1972,6 +2064,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2003,6 +2097,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2034,6 +2130,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2132,6 +2230,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Embedded Data Systems',
'model': 'EDS0066',
'name': '7E.222222222222',
@ -2163,6 +2263,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2194,6 +2296,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2262,6 +2366,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Hobby Boards',
'model': 'HobbyBoards_EF',
'name': 'EF.111111111111',
@ -2293,6 +2399,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2324,6 +2432,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2355,6 +2465,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2438,6 +2550,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Hobby Boards',
'model': 'HB_MOISTURE_METER',
'name': 'EF.111111111112',
@ -2469,6 +2583,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2500,6 +2616,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2531,6 +2649,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2562,6 +2682,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2660,6 +2782,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Hobby Boards',
'model': 'HB_HUB',
'name': 'EF.111111111113',

View file

@ -30,6 +30,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2405',
'name': '05.111111111111',
@ -59,6 +61,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -109,6 +113,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18S20',
'name': '10.111111111111',
@ -147,6 +153,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2406',
'name': '12.111111111111',
@ -176,6 +184,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -205,6 +215,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -234,6 +246,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -263,6 +277,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -349,6 +365,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2423',
'name': '1D.111111111111',
@ -387,6 +405,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2409',
'name': '1F.111111111111',
@ -413,6 +433,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2423',
'name': '1D.111111111111',
@ -451,6 +473,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS1822',
'name': '22.111111111111',
@ -489,6 +513,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2438',
'name': '26.111111111111',
@ -518,6 +544,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -568,6 +596,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18B20',
'name': '28.111111111111',
@ -606,6 +636,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18B20',
'name': '28.222222222222',
@ -644,6 +676,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS18B20',
'name': '28.222222222223',
@ -682,6 +716,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2408',
'name': '29.111111111111',
@ -711,6 +747,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -740,6 +778,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -769,6 +809,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -798,6 +840,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -827,6 +871,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -856,6 +902,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -885,6 +933,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -914,6 +964,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -943,6 +995,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -972,6 +1026,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1001,6 +1057,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1030,6 +1088,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1059,6 +1119,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1088,6 +1150,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1117,6 +1181,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1146,6 +1212,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1376,6 +1444,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2760',
'name': '30.111111111111',
@ -1414,6 +1484,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS2413',
'name': '3A.111111111111',
@ -1443,6 +1515,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1472,6 +1546,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1534,6 +1610,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS1825',
'name': '3B.111111111111',
@ -1572,6 +1650,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Maxim Integrated',
'model': 'DS28EA00',
'name': '42.111111111111',
@ -1610,6 +1690,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Embedded Data Systems',
'model': 'EDS0068',
'name': '7E.111111111111',
@ -1648,6 +1730,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Embedded Data Systems',
'model': 'EDS0066',
'name': '7E.222222222222',
@ -1686,6 +1770,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Hobby Boards',
'model': 'HobbyBoards_EF',
'name': 'EF.111111111111',
@ -1724,6 +1810,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Hobby Boards',
'model': 'HB_MOISTURE_METER',
'name': 'EF.111111111112',
@ -1753,6 +1841,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1782,6 +1872,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1811,6 +1903,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1840,6 +1934,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1869,6 +1965,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1898,6 +1996,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1927,6 +2027,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1956,6 +2058,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2090,6 +2194,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Hobby Boards',
'model': 'HB_HUB',
'name': 'EF.111111111113',
@ -2119,6 +2225,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2148,6 +2256,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2177,6 +2287,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2206,6 +2318,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -47,6 +49,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -76,6 +80,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -105,6 +111,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -134,6 +142,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -163,6 +173,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -192,6 +204,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -296,6 +310,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -325,6 +341,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -354,6 +372,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -383,6 +403,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -412,6 +434,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -441,6 +465,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -470,6 +496,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -499,6 +527,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -528,6 +558,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -654,6 +686,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -683,6 +717,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -712,6 +748,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -741,6 +779,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -812,6 +852,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -841,6 +883,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -870,6 +914,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -899,6 +945,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -928,6 +976,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -957,6 +1007,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -986,6 +1038,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1015,6 +1069,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1044,6 +1100,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1073,6 +1131,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1210,6 +1270,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -1239,6 +1301,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1268,6 +1332,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1297,6 +1363,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1326,6 +1394,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1355,6 +1425,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1384,6 +1456,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1488,6 +1562,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -1517,6 +1593,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1546,6 +1624,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1575,6 +1655,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1604,6 +1686,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1633,6 +1717,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1662,6 +1748,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1691,6 +1779,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1720,6 +1810,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1846,6 +1938,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -1875,6 +1969,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1904,6 +2000,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1933,6 +2031,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2004,6 +2104,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -2033,6 +2135,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2062,6 +2166,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2091,6 +2197,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2120,6 +2228,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2149,6 +2259,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2178,6 +2290,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2207,6 +2321,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2236,6 +2352,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2265,6 +2383,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -47,6 +49,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -96,6 +100,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -125,6 +131,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -154,6 +162,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -183,6 +193,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -254,6 +266,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -283,6 +297,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -312,6 +328,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -341,6 +359,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -412,6 +432,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -441,6 +463,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -470,6 +494,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -499,6 +525,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -570,6 +598,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -599,6 +629,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -648,6 +680,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -677,6 +711,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -706,6 +742,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -735,6 +773,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -806,6 +846,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -835,6 +877,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -864,6 +908,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -893,6 +939,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -964,6 +1012,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -993,6 +1043,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1022,6 +1074,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1051,6 +1105,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -47,6 +49,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -97,6 +101,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -126,6 +132,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -176,6 +184,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -214,6 +224,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -243,6 +255,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -293,6 +307,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -322,6 +338,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -375,6 +393,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -404,6 +424,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -457,6 +479,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -495,6 +519,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -524,6 +550,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -56,6 +58,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -91,6 +95,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -145,6 +151,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -180,6 +188,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -234,6 +244,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -269,6 +281,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -323,6 +337,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -361,6 +377,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -396,6 +414,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -450,6 +470,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -485,6 +507,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -539,6 +563,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -574,6 +600,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -49,6 +51,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -80,6 +84,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -111,6 +117,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -140,6 +148,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -169,6 +179,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -198,6 +210,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -309,6 +323,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -340,6 +356,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -380,6 +398,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -411,6 +431,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -442,6 +464,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -478,6 +502,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -509,6 +535,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -540,6 +568,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -571,6 +601,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -600,6 +632,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -631,6 +665,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -662,6 +698,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -693,6 +731,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -722,6 +762,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -751,6 +793,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -780,6 +824,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1022,6 +1068,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -1053,6 +1101,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1093,6 +1143,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1124,6 +1176,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1155,6 +1209,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1191,6 +1247,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1222,6 +1280,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1253,6 +1313,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1284,6 +1346,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1313,6 +1377,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1344,6 +1410,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1375,6 +1443,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1404,6 +1474,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1433,6 +1505,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1462,6 +1536,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1491,6 +1567,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1729,6 +1807,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -1760,6 +1840,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1800,6 +1882,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1831,6 +1915,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1862,6 +1948,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1898,6 +1986,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1929,6 +2019,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1960,6 +2052,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -1991,6 +2085,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2020,6 +2116,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2051,6 +2149,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2082,6 +2182,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2111,6 +2213,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2140,6 +2244,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2169,6 +2275,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2198,6 +2306,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2227,6 +2337,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2476,6 +2588,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -2507,6 +2621,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2538,6 +2654,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2569,6 +2687,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2598,6 +2718,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2627,6 +2749,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2656,6 +2780,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2767,6 +2893,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Captur ii',
'name': 'REG-NUMBER',
@ -2798,6 +2926,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2838,6 +2968,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2869,6 +3001,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2900,6 +3034,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2936,6 +3072,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2967,6 +3105,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -2998,6 +3138,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3029,6 +3171,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3058,6 +3202,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3089,6 +3235,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3120,6 +3268,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3151,6 +3301,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3180,6 +3332,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3209,6 +3363,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3238,6 +3394,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3480,6 +3638,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -3511,6 +3671,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3551,6 +3713,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3582,6 +3746,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3613,6 +3779,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3649,6 +3817,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3680,6 +3850,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3711,6 +3883,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3742,6 +3916,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3771,6 +3947,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3802,6 +3980,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3833,6 +4013,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3862,6 +4044,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3891,6 +4075,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3920,6 +4106,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -3949,6 +4137,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4187,6 +4377,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Renault',
'model': 'Zoe',
'name': 'REG-NUMBER',
@ -4218,6 +4410,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4258,6 +4452,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4289,6 +4485,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4320,6 +4518,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4356,6 +4556,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4387,6 +4589,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4418,6 +4622,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4449,6 +4655,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4478,6 +4686,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4509,6 +4719,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4540,6 +4752,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4569,6 +4783,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4598,6 +4814,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4627,6 +4845,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4656,6 +4876,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -4685,6 +4907,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -40,6 +40,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': 'NB6VAC-FXC-r0',
'name': 'SFR Box',
@ -47,6 +49,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -76,6 +80,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -136,6 +142,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': 'NB6VAC-FXC-r0',
'name': 'SFR Box',
@ -165,6 +173,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -194,6 +204,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': 'NB6VAC-FXC-r0',
'name': 'SFR Box',
@ -47,6 +49,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': 'NB6VAC-FXC-r0',
'name': 'SFR Box',
@ -54,6 +56,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -83,6 +87,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -112,6 +118,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -149,6 +157,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -178,6 +188,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -207,6 +219,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -236,6 +250,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -267,6 +283,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -298,6 +316,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -329,6 +349,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -360,6 +382,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -391,6 +415,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -422,6 +448,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -460,6 +488,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -502,6 +532,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -29,6 +29,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -73,6 +75,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -117,6 +121,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -161,6 +167,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -205,6 +213,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -249,6 +259,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -293,6 +305,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -337,6 +351,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -61,6 +61,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -93,6 +95,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Twente Milieu',
'model': None,
'name': 'Twente Milieu',

View file

@ -30,6 +30,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -62,6 +64,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Twente Milieu',
'model': None,
'name': 'Twente Milieu',
@ -103,6 +107,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -135,6 +141,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Twente Milieu',
'model': None,
'name': 'Twente Milieu',
@ -176,6 +184,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -208,6 +218,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Twente Milieu',
'model': None,
'name': 'Twente Milieu',
@ -249,6 +261,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -281,6 +295,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Twente Milieu',
'model': None,
'name': 'Twente Milieu',
@ -322,6 +338,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -354,6 +372,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Twente Milieu',
'model': None,
'name': 'Twente Milieu',

View file

@ -29,6 +29,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -54,6 +56,8 @@
'hw_version': None,
'id': <ANY>,
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'name': 'Uptime',

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LV-PUR131S',
'name': 'Air Purifier 131s',
@ -52,6 +54,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -103,6 +107,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'Core200S',
'name': 'Air Purifier 200s',
@ -136,6 +142,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -193,6 +201,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LAP-C401S-WJP',
'name': 'Air Purifier 400s',
@ -227,6 +237,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -285,6 +297,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LAP-C601S-WUS',
'name': 'Air Purifier 600s',
@ -319,6 +333,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -377,6 +393,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESL100',
'name': 'Dimmable Light',
@ -411,6 +429,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESWD16',
'name': 'Dimmer Switch',
@ -461,6 +481,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'wifi-switch-1.3',
'name': 'Outlet',
@ -495,6 +517,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESL100CW',
'name': 'Temperature Light',
@ -529,6 +553,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESWL01',
'name': 'Wall Switch',

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LV-PUR131S',
'name': 'Air Purifier 131s',
@ -52,6 +54,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'Core200S',
'name': 'Air Purifier 200s',
@ -86,6 +90,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LAP-C401S-WJP',
'name': 'Air Purifier 400s',
@ -120,6 +126,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LAP-C601S-WUS',
'name': 'Air Purifier 600s',
@ -154,6 +162,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESL100',
'name': 'Dimmable Light',
@ -187,6 +197,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -237,6 +249,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESWD16',
'name': 'Dimmer Switch',
@ -270,6 +284,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -338,6 +354,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'wifi-switch-1.3',
'name': 'Outlet',
@ -372,6 +390,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESL100CW',
'name': 'Temperature Light',
@ -409,6 +429,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -470,6 +492,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESWL01',
'name': 'Wall Switch',

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LV-PUR131S',
'name': 'Air Purifier 131s',
@ -49,6 +51,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -78,6 +82,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -138,6 +144,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'Core200S',
'name': 'Air Purifier 200s',
@ -169,6 +177,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -217,6 +227,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LAP-C401S-WJP',
'name': 'Air Purifier 400s',
@ -248,6 +260,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -277,6 +291,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -308,6 +324,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -383,6 +401,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LAP-C601S-WUS',
'name': 'Air Purifier 600s',
@ -414,6 +434,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -443,6 +465,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -474,6 +498,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -549,6 +575,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESL100',
'name': 'Dimmable Light',
@ -583,6 +611,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESWD16',
'name': 'Dimmer Switch',
@ -633,6 +663,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'wifi-switch-1.3',
'name': 'Outlet',
@ -664,6 +696,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -695,6 +729,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -726,6 +762,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -757,6 +795,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -788,6 +828,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -819,6 +861,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -943,6 +987,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESL100CW',
'name': 'Temperature Light',
@ -977,6 +1023,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESWL01',
'name': 'Wall Switch',

View file

@ -18,6 +18,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LV-PUR131S',
'name': 'Air Purifier 131s',
@ -52,6 +54,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'Core200S',
'name': 'Air Purifier 200s',
@ -86,6 +90,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LAP-C401S-WJP',
'name': 'Air Purifier 400s',
@ -120,6 +126,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LAP-C601S-WUS',
'name': 'Air Purifier 600s',
@ -154,6 +162,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESL100',
'name': 'Dimmable Light',
@ -188,6 +198,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESWD16',
'name': 'Dimmer Switch',
@ -238,6 +250,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'wifi-switch-1.3',
'name': 'Outlet',
@ -267,6 +281,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -313,6 +329,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESL100CW',
'name': 'Temperature Light',
@ -347,6 +365,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'ESWL01',
'name': 'Wall Switch',
@ -376,6 +396,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -29,6 +29,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -61,6 +63,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'name': 'home-assistant.io',
@ -101,6 +105,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -133,6 +139,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'name': 'home-assistant.io',
@ -178,6 +186,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -210,6 +220,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'name': 'home-assistant.io',
@ -250,6 +262,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -282,6 +296,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'name': 'home-assistant.io',
@ -322,6 +338,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -354,6 +372,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'name': 'home-assistant.io',
@ -394,6 +414,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -426,6 +448,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'name': 'home-assistant.io',
@ -466,6 +490,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -498,6 +524,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'name': 'home-assistant.io',
@ -538,6 +566,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -570,6 +600,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'name': 'home-assistant.io',
@ -610,6 +642,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -642,6 +676,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'name': 'home-assistant.io',
@ -682,6 +718,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),

View file

@ -29,6 +29,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -65,6 +67,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGB Light',

View file

@ -29,6 +29,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -65,6 +67,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGB Light',

View file

@ -37,6 +37,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -73,6 +75,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGB Light',
@ -121,6 +125,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -157,6 +163,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGB Light',

View file

@ -39,6 +39,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -75,6 +77,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGB Light',
@ -219,6 +223,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -255,6 +261,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGB Light',
@ -303,6 +311,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -339,6 +349,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGBW Light',
@ -387,6 +399,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -423,6 +437,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGBW Light',

View file

@ -31,6 +31,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -67,6 +69,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGB Light',
@ -106,6 +110,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -142,6 +148,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGB Light',
@ -182,6 +190,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -218,6 +228,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGB Light',
@ -258,6 +270,8 @@
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
@ -294,6 +308,8 @@
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WLED',
'model': 'DIY light',
'name': 'WLED RGB Light',

View file

@ -56,6 +56,7 @@ from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
label_registry as lr,
recorder as recorder_helper,
)
from homeassistant.helpers.typing import ConfigType
@ -1614,6 +1615,12 @@ def issue_registry(hass: HomeAssistant) -> ir.IssueRegistry:
return ir.async_get(hass)
@pytest.fixture
def label_registry(hass: HomeAssistant) -> lr.LabelRegistry:
"""Return the label registry from the current hass instance."""
return lr.async_get(hass)
@pytest.fixture
def snapshot(snapshot: SnapshotAssertion) -> SnapshotAssertion:
"""Return snapshot assertion fixture with the Home Assistant extension."""

View file

@ -204,6 +204,7 @@ async def test_loading_from_storage(
"hw_version": "hw_version",
"id": "abcdefghijklm",
"identifiers": [["serial", "123456ABCDEF"]],
"labels": {"label1", "label2"},
"manufacturer": "manufacturer",
"model": "model",
"name_by_user": "Test Friendly Name",
@ -247,6 +248,7 @@ async def test_loading_from_storage(
hw_version="hw_version",
id="abcdefghijklm",
identifiers={("serial", "123456ABCDEF")},
labels={"label1", "label2"},
manufacturer="manufacturer",
model="model",
name_by_user="Test Friendly Name",
@ -282,12 +284,12 @@ async def test_loading_from_storage(
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_1_to_1_4(
async def test_migration_1_1_to_1_5(
hass: HomeAssistant,
hass_storage: dict[str, Any],
mock_config_entry: MockConfigEntry,
) -> None:
"""Test migration from version 1.1 to 1.4."""
"""Test migration from version 1.1 to 1.5."""
hass_storage[dr.STORAGE_KEY] = {
"version": 1,
"minor_version": 1,
@ -371,6 +373,7 @@ async def test_migration_1_1_to_1_4(
"hw_version": None,
"id": "abcdefghijklm",
"identifiers": [["serial", "123456ABCDEF"]],
"labels": [],
"manufacturer": "manufacturer",
"model": "model",
"name": "name",
@ -389,6 +392,7 @@ async def test_migration_1_1_to_1_4(
"hw_version": None,
"id": "invalid-entry-type",
"identifiers": [["serial", "mock-id-invalid-entry"]],
"labels": [],
"manufacturer": None,
"model": None,
"name_by_user": None,
@ -412,12 +416,12 @@ async def test_migration_1_1_to_1_4(
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_2_to_1_4(
async def test_migration_1_2_to_1_5(
hass: HomeAssistant,
hass_storage: dict[str, Any],
mock_config_entry: MockConfigEntry,
) -> None:
"""Test migration from version 1.2 to 1.3."""
"""Test migration from version 1.2 to 1.5."""
hass_storage[dr.STORAGE_KEY] = {
"version": 1,
"minor_version": 2,
@ -500,6 +504,7 @@ async def test_migration_1_2_to_1_4(
"hw_version": None,
"id": "abcdefghijklm",
"identifiers": [["serial", "123456ABCDEF"]],
"labels": [],
"manufacturer": "manufacturer",
"model": "model",
"name": "name",
@ -518,6 +523,7 @@ async def test_migration_1_2_to_1_4(
"hw_version": None,
"id": "invalid-entry-type",
"identifiers": [["serial", "mock-id-invalid-entry"]],
"labels": [],
"manufacturer": None,
"model": None,
"name_by_user": None,
@ -533,12 +539,12 @@ async def test_migration_1_2_to_1_4(
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_3_to_1_4(
async def test_migration_1_3_to_1_5(
hass: HomeAssistant,
hass_storage: dict[str, Any],
mock_config_entry: MockConfigEntry,
):
"""Test migration from version 1.3 to 1.4."""
"""Test migration from version 1.3 to 1.5."""
hass_storage[dr.STORAGE_KEY] = {
"version": 1,
"minor_version": 3,
@ -623,6 +629,7 @@ async def test_migration_1_3_to_1_4(
"hw_version": "hw_version",
"id": "abcdefghijklm",
"identifiers": [["serial", "123456ABCDEF"]],
"labels": [],
"manufacturer": "manufacturer",
"model": "model",
"name": "name",
@ -641,6 +648,134 @@ async def test_migration_1_3_to_1_4(
"hw_version": None,
"id": "invalid-entry-type",
"identifiers": [["serial", "mock-id-invalid-entry"]],
"labels": [],
"manufacturer": None,
"model": None,
"name_by_user": None,
"name": None,
"serial_number": None,
"sw_version": None,
"via_device_id": None,
},
],
"deleted_devices": [],
},
}
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_4_to_1_5(
hass: HomeAssistant,
hass_storage: dict[str, Any],
mock_config_entry: MockConfigEntry,
):
"""Test migration from version 1.4 to 1.5."""
hass_storage[dr.STORAGE_KEY] = {
"version": 1,
"minor_version": 3,
"key": dr.STORAGE_KEY,
"data": {
"devices": [
{
"area_id": None,
"config_entries": [mock_config_entry.entry_id],
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"disabled_by": None,
"entry_type": "service",
"hw_version": "hw_version",
"id": "abcdefghijklm",
"identifiers": [["serial", "123456ABCDEF"]],
"manufacturer": "manufacturer",
"model": "model",
"name": "name",
"name_by_user": None,
"serial_number": None,
"sw_version": "version",
"via_device_id": None,
},
{
"area_id": None,
"config_entries": [None],
"configuration_url": None,
"connections": [],
"disabled_by": None,
"entry_type": None,
"hw_version": None,
"id": "invalid-entry-type",
"identifiers": [["serial", "mock-id-invalid-entry"]],
"manufacturer": None,
"model": None,
"name_by_user": None,
"name": None,
"serial_number": None,
"sw_version": None,
"via_device_id": None,
},
],
"deleted_devices": [],
},
}
await dr.async_load(hass)
registry = dr.async_get(hass)
# Test data was loaded
entry = registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
connections={("Zigbee", "01.23.45.67.89")},
identifiers={("serial", "123456ABCDEF")},
)
assert entry.id == "abcdefghijklm"
# Update to trigger a store
entry = registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
connections={("Zigbee", "01.23.45.67.89")},
identifiers={("serial", "123456ABCDEF")},
sw_version="new_version",
)
assert entry.id == "abcdefghijklm"
# Check we store migrated data
await flush_store(registry._store)
assert hass_storage[dr.STORAGE_KEY] == {
"version": dr.STORAGE_VERSION_MAJOR,
"minor_version": dr.STORAGE_VERSION_MINOR,
"key": dr.STORAGE_KEY,
"data": {
"devices": [
{
"area_id": None,
"config_entries": [mock_config_entry.entry_id],
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"disabled_by": None,
"entry_type": "service",
"hw_version": "hw_version",
"id": "abcdefghijklm",
"identifiers": [["serial", "123456ABCDEF"]],
"labels": [],
"manufacturer": "manufacturer",
"model": "model",
"name": "name",
"name_by_user": None,
"serial_number": None,
"sw_version": "new_version",
"via_device_id": None,
},
{
"area_id": None,
"config_entries": [None],
"configuration_url": None,
"connections": [],
"disabled_by": None,
"entry_type": None,
"hw_version": None,
"id": "invalid-entry-type",
"identifiers": [["serial", "mock-id-invalid-entry"]],
"labels": [],
"manufacturer": None,
"model": None,
"name_by_user": None,
@ -1007,7 +1142,10 @@ async def test_loading_saving_data(
assert len(device_registry.deleted_devices) == 1
orig_via = device_registry.async_update_device(
orig_via.id, area_id="mock-area-id", name_by_user="mock-name-by-user"
orig_via.id,
area_id="mock-area-id",
name_by_user="mock-name-by-user",
labels={"mock-label1", "mock-label2"},
)
# Now load written data in new registry
@ -1115,6 +1253,7 @@ async def test_update(
)
new_identifiers = {("hue", "654"), ("bla", "321")}
assert not entry.area_id
assert not entry.labels
assert not entry.name_by_user
with patch.object(device_registry, "async_schedule_save") as mock_save:
@ -1125,6 +1264,7 @@ async def test_update(
disabled_by=dr.DeviceEntryDisabler.USER,
entry_type=dr.DeviceEntryType.SERVICE,
hw_version="hw_version",
labels={"label1", "label2"},
manufacturer="Test Producer",
model="Test Model",
name_by_user="Test Friendly Name",
@ -1148,6 +1288,7 @@ async def test_update(
hw_version="hw_version",
id=entry.id,
identifiers={("bla", "321"), ("hue", "654")},
labels={"label1", "label2"},
manufacturer="Test Producer",
model="Test Model",
name_by_user="Test Friendly Name",
@ -1192,6 +1333,7 @@ async def test_update(
"entry_type": None,
"hw_version": None,
"identifiers": {("bla", "123"), ("hue", "456")},
"labels": set(),
"manufacturer": None,
"model": None,
"name": None,
@ -1996,6 +2138,7 @@ async def test_loading_invalid_configuration_url_from_storage(
"hw_version": None,
"id": "abcdefghijklm",
"identifiers": [["serial", "123456ABCDEF"]],
"labels": [],
"manufacturer": None,
"model": None,
"name_by_user": None,
@ -2031,3 +2174,87 @@ def test_deprecated_constants(
) -> None:
"""Test deprecated constants."""
import_and_test_deprecated_constant_enum(caplog, dr, enum, "DISABLED_", "2025.1")
async def test_removing_labels(
hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
"""Make sure we can clear labels."""
config_entry = MockConfigEntry()
config_entry.add_to_hass(hass)
entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
identifiers={("bridgeid", "0123")},
manufacturer="manufacturer",
model="model",
)
entry = device_registry.async_update_device(entry.id, labels={"label1", "label2"})
device_registry.async_clear_label_id("label1")
entry_cleared_label1 = device_registry.async_get_device({("bridgeid", "0123")})
device_registry.async_clear_label_id("label2")
entry_cleared_label2 = device_registry.async_get_device({("bridgeid", "0123")})
assert entry_cleared_label1
assert entry_cleared_label2
assert entry != entry_cleared_label1
assert entry != entry_cleared_label2
assert entry_cleared_label1 != entry_cleared_label2
assert entry.labels == {"label1", "label2"}
assert entry_cleared_label1.labels == {"label2"}
assert not entry_cleared_label2.labels
async def test_entries_for_label(
hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
"""Test getting device entries by label."""
config_entry = MockConfigEntry()
config_entry.add_to_hass(hass)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:00")},
identifiers={("bridgeid", "0000")},
manufacturer="manufacturer",
model="model",
)
entry_1 = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:23")},
identifiers={("bridgeid", "0123")},
manufacturer="manufacturer",
model="model",
)
entry_1 = device_registry.async_update_device(entry_1.id, labels={"label1"})
entry_2 = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:56")},
identifiers={("bridgeid", "0456")},
manufacturer="manufacturer",
model="model",
)
entry_2 = device_registry.async_update_device(entry_2.id, labels={"label2"})
entry_1_and_2 = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:89")},
identifiers={("bridgeid", "0789")},
manufacturer="manufacturer",
model="model",
)
entry_1_and_2 = device_registry.async_update_device(
entry_1_and_2.id, labels={"label1", "label2"}
)
entries = dr.async_entries_for_label(device_registry, "label1")
assert len(entries) == 2
assert entries == [entry_1, entry_1_and_2]
entries = dr.async_entries_for_label(device_registry, "label2")
assert len(entries) == 2
assert entries == [entry_2, entry_1_and_2]
assert not dr.async_entries_for_label(device_registry, "unknown")
assert not dr.async_entries_for_label(device_registry, "")

View file

@ -277,6 +277,9 @@ async def test_loading_saving_data(
entity_registry.async_update_entity_options(
orig_entry2.entity_id, "light", {"minimum_brightness": 20}
)
entity_registry.async_update_entity(
orig_entry2.entity_id, labels={"label1", "label2"}
)
orig_entry2 = entity_registry.async_get(orig_entry2.entity_id)
orig_entry3 = entity_registry.async_get_or_create("light", "hue", "ABCD")
orig_entry4 = entity_registry.async_get_or_create("light", "hue", "EFGH")
@ -314,6 +317,7 @@ async def test_loading_saving_data(
assert new_entry2.icon == "hass:user-icon"
assert new_entry2.hidden_by == er.RegistryEntryHider.INTEGRATION
assert new_entry2.has_entity_name is True
assert new_entry2.labels == {"label1", "label2"}
assert new_entry2.name == "User Name"
assert new_entry2.options == {"light": {"minimum_brightness": 20}}
assert new_entry2.original_device_class == "mock-device-class"
@ -1756,3 +1760,70 @@ async def test_async_migrate_entry_delete_other(
await er.async_migrate_entries(hass, config_entry1.entry_id, _async_migrator)
assert entries == {entry1.entity_id}
assert not entity_registry.async_is_registered(entry2.entity_id)
async def test_removing_labels(entity_registry: er.EntityRegistry) -> None:
"""Make sure we can clear labels."""
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="5678",
)
entry = entity_registry.async_update_entity(
entry.entity_id, labels={"label1", "label2"}
)
entity_registry.async_clear_label_id("label1")
entry_cleared_label1 = entity_registry.async_get(entry.entity_id)
entity_registry.async_clear_label_id("label2")
entry_cleared_label2 = entity_registry.async_get(entry.entity_id)
assert entry_cleared_label1
assert entry_cleared_label2
assert entry != entry_cleared_label1
assert entry != entry_cleared_label2
assert entry_cleared_label1 != entry_cleared_label2
assert entry.labels == {"label1", "label2"}
assert entry_cleared_label1.labels == {"label2"}
assert not entry_cleared_label2.labels
async def test_entries_for_label(entity_registry: er.EntityRegistry) -> None:
"""Test getting entity entries by label."""
entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="000",
)
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="123",
)
label_1 = entity_registry.async_update_entity(entry.entity_id, labels={"label1"})
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="456",
)
label_2 = entity_registry.async_update_entity(entry.entity_id, labels={"label2"})
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="789",
)
label_1_and_2 = entity_registry.async_update_entity(
entry.entity_id, labels={"label1", "label2"}
)
entries = er.async_entries_for_label(entity_registry, "label1")
assert len(entries) == 2
assert entries == [label_1, label_1_and_2]
entries = er.async_entries_for_label(entity_registry, "label2")
assert len(entries) == 2
assert entries == [label_2, label_1_and_2]
assert not er.async_entries_for_label(entity_registry, "unknown")
assert not er.async_entries_for_label(entity_registry, "")

View file

@ -0,0 +1,460 @@
"""Tests for the Label Registry."""
import re
from typing import Any
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
label_registry as lr,
)
from homeassistant.helpers.label_registry import (
EVENT_LABEL_REGISTRY_UPDATED,
STORAGE_KEY,
STORAGE_VERSION_MAJOR,
LabelRegistry,
async_get,
async_load,
)
from tests.common import MockConfigEntry, async_capture_events, flush_store
async def test_list_labels(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can read label."""
labels = label_registry.async_list_labels()
assert len(list(labels)) == len(label_registry.labels)
async def test_create_label(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can create labels."""
update_events = async_capture_events(hass, EVENT_LABEL_REGISTRY_UPDATED)
label = label_registry.async_create(
name="My Label",
color="#FF0000",
icon="mdi:test",
description="This label is for testing",
)
assert label.label_id == "my_label"
assert label.name == "My Label"
assert label.color == "#FF0000"
assert label.icon == "mdi:test"
assert label.description == "This label is for testing"
assert len(label_registry.labels) == 1
await hass.async_block_till_done()
assert len(update_events) == 1
assert update_events[0].data == {
"action": "create",
"label_id": label.label_id,
}
async def test_create_label_with_name_already_in_use(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can't create an label with a name already in use."""
update_events = async_capture_events(hass, EVENT_LABEL_REGISTRY_UPDATED)
label_registry.async_create("mock")
with pytest.raises(
ValueError, match=re.escape("The name mock (mock) is already in use")
):
label_registry.async_create("mock")
await hass.async_block_till_done()
assert len(label_registry.labels) == 1
assert len(update_events) == 1
async def test_create_label_with_id_already_in_use(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can't create an label with a name already in use."""
label = label_registry.async_create("Label")
updated_label = label_registry.async_update(label.label_id, name="Renamed Label")
assert updated_label.label_id == label.label_id
second_label = label_registry.async_create("Label")
assert label.label_id != second_label.label_id
assert second_label.label_id == "label_2"
async def test_delete_label(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can delete an label."""
update_events = async_capture_events(hass, EVENT_LABEL_REGISTRY_UPDATED)
label = label_registry.async_create("Label")
assert len(label_registry.labels) == 1
label_registry.async_delete(label.label_id)
assert not label_registry.labels
await hass.async_block_till_done()
assert len(update_events) == 2
assert update_events[0].data == {
"action": "create",
"label_id": label.label_id,
}
assert update_events[1].data == {
"action": "remove",
"label_id": label.label_id,
}
async def test_delete_non_existing_label(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can't delete an label that doesn't exist."""
label_registry.async_create("mock")
with pytest.raises(KeyError):
label_registry.async_delete("")
assert len(label_registry.labels) == 1
async def test_update_label(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can update labels."""
update_events = async_capture_events(hass, EVENT_LABEL_REGISTRY_UPDATED)
label = label_registry.async_create("Mock")
assert len(label_registry.labels) == 1
assert label.label_id == "mock"
assert label.name == "Mock"
assert label.color is None
assert label.icon is None
assert label.description is None
updated_label = label_registry.async_update(
label.label_id,
name="Updated",
color="#FFFFFF",
icon="mdi:update",
description="Updated description",
)
assert updated_label != label
assert updated_label.label_id == "mock"
assert updated_label.name == "Updated"
assert updated_label.color == "#FFFFFF"
assert updated_label.icon == "mdi:update"
assert updated_label.description == "Updated description"
assert len(label_registry.labels) == 1
await hass.async_block_till_done()
assert len(update_events) == 2
assert update_events[0].data == {
"action": "create",
"label_id": label.label_id,
}
assert update_events[1].data == {
"action": "update",
"label_id": label.label_id,
}
async def test_update_label_with_same_data(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can reapply the same data to the label and it won't update."""
update_events = async_capture_events(hass, EVENT_LABEL_REGISTRY_UPDATED)
label = label_registry.async_create(
"mock",
color="#FFFFFF",
icon="mdi:test",
description="Description",
)
udpated_label = label_registry.async_update(
label_id=label.label_id,
name="mock",
color="#FFFFFF",
icon="mdi:test",
description="Description",
)
assert label == udpated_label
await hass.async_block_till_done()
# No update event
assert len(update_events) == 1
assert update_events[0].data == {
"action": "create",
"label_id": label.label_id,
}
async def test_update_label_with_same_name_change_case(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can reapply the same name with a different case to the label."""
label = label_registry.async_create("mock")
updated_label = label_registry.async_update(label.label_id, name="Mock")
assert updated_label.name == "Mock"
assert updated_label.label_id == label.label_id
assert updated_label.normalized_name == label.normalized_name
assert len(label_registry.labels) == 1
async def test_update_label_with_name_already_in_use(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can't update an label with a name already in use."""
label1 = label_registry.async_create("mock1")
label2 = label_registry.async_create("mock2")
with pytest.raises(
ValueError, match=re.escape("The name mock2 (mock2) is already in use")
):
label_registry.async_update(label1.label_id, name="mock2")
assert label1.name == "mock1"
assert label2.name == "mock2"
assert len(label_registry.labels) == 2
async def test_update_label_with_normalized_name_already_in_use(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can't update an label with a normalized name already in use."""
label1 = label_registry.async_create("mock1")
label2 = label_registry.async_create("M O C K 2")
with pytest.raises(
ValueError, match=re.escape("The name mock2 (mock2) is already in use")
):
label_registry.async_update(label1.label_id, name="mock2")
assert label1.name == "mock1"
assert label2.name == "M O C K 2"
assert len(label_registry.labels) == 2
async def test_load_labels(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure that we can load/save data correctly."""
label1 = label_registry.async_create(
"Label One",
color="#FF000",
icon="mdi:one",
description="This label is label one",
)
label2 = label_registry.async_create(
"Label Two",
color="#000FF",
icon="mdi:two",
description="This label is label two",
)
assert len(label_registry.labels) == 2
registry2 = LabelRegistry(hass)
await flush_store(label_registry._store)
await registry2.async_load()
assert len(registry2.labels) == 2
assert list(label_registry.labels) == list(registry2.labels)
label1_registry2 = registry2.async_get_or_create("Label One")
assert label1_registry2.label_id == label1.label_id
assert label1_registry2.name == label1.name
assert label1_registry2.color == label1.color
assert label1_registry2.description == label1.description
assert label1_registry2.icon == label1.icon
assert label1_registry2.normalized_name == label1.normalized_name
label2_registry2 = registry2.async_get_or_create("Label Two")
assert label2_registry2.name == label2.name
assert label2_registry2.color == label2.color
assert label2_registry2.description == label2.description
assert label2_registry2.icon == label2.icon
assert label2_registry2.normalized_name == label2.normalized_name
@pytest.mark.parametrize("load_registries", [False])
async def test_loading_label_from_storage(
hass: HomeAssistant, hass_storage: Any
) -> None:
"""Test loading stored labels on start."""
hass_storage[STORAGE_KEY] = {
"version": STORAGE_VERSION_MAJOR,
"data": {
"labels": [
{
"color": "#FFFFFF",
"description": None,
"icon": "mdi:test",
"label_id": "one",
"name": "One",
}
]
},
}
await async_load(hass)
registry = async_get(hass)
assert len(registry.labels) == 1
async def test_getting_label(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure we can get the labels by name."""
label = label_registry.async_get_or_create("Mock1")
label2 = label_registry.async_get_or_create("mock1")
label3 = label_registry.async_get_or_create("mock 1")
assert label == label2
assert label == label3
assert label2 == label3
get_label = label_registry.async_get_label_by_name("M o c k 1")
assert get_label == label
get_label = label_registry.async_get_label(label.label_id)
assert get_label == label
async def test_async_get_label_by_name_not_found(
hass: HomeAssistant, label_registry: lr.LabelRegistry
) -> None:
"""Make sure we return None for non-existent labels."""
label_registry.async_create("Mock1")
assert len(label_registry.labels) == 1
assert label_registry.async_get_label_by_name("non_exist") is None
async def test_labels_removed_from_devices(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
label_registry: lr.LabelRegistry,
) -> None:
"""Tests if label gets removed from devices when the label is removed."""
config_entry = MockConfigEntry()
config_entry.add_to_hass(hass)
label1 = label_registry.async_create("label1")
label2 = label_registry.async_create("label2")
assert len(label_registry.labels) == 2
entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:23")},
identifiers={("bridgeid", "0123")},
manufacturer="manufacturer",
model="model",
)
device_registry.async_update_device(entry.id, labels={label1.label_id})
entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:56")},
identifiers={("bridgeid", "0456")},
manufacturer="manufacturer",
model="model",
)
device_registry.async_update_device(entry.id, labels={label2.label_id})
entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:89")},
identifiers={("bridgeid", "0789")},
manufacturer="manufacturer",
model="model",
)
device_registry.async_update_device(
entry.id, labels={label1.label_id, label2.label_id}
)
entries = dr.async_entries_for_label(device_registry, label1.label_id)
assert len(entries) == 2
entries = dr.async_entries_for_label(device_registry, label2.label_id)
assert len(entries) == 2
label_registry.async_delete(label1.label_id)
entries = dr.async_entries_for_label(device_registry, label1.label_id)
assert len(entries) == 0
entries = dr.async_entries_for_label(device_registry, label2.label_id)
assert len(entries) == 2
label_registry.async_delete(label2.label_id)
entries = dr.async_entries_for_label(device_registry, label1.label_id)
assert len(entries) == 0
entries = dr.async_entries_for_label(device_registry, label2.label_id)
assert len(entries) == 0
async def test_labels_removed_from_entities(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
label_registry: lr.LabelRegistry,
) -> None:
"""Tests if label gets removed from entity when the label is removed."""
label1 = label_registry.async_create("label1")
label2 = label_registry.async_create("label2")
assert len(label_registry.labels) == 2
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="123",
)
entity_registry.async_update_entity(entry.entity_id, labels={label1.label_id})
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="456",
)
entity_registry.async_update_entity(entry.entity_id, labels={label2.label_id})
entry = entity_registry.async_get_or_create(
domain="light",
platform="hue",
unique_id="789",
)
entity_registry.async_update_entity(
entry.entity_id, labels={label1.label_id, label2.label_id}
)
entries = er.async_entries_for_label(entity_registry, label1.label_id)
assert len(entries) == 2
entries = er.async_entries_for_label(entity_registry, label2.label_id)
assert len(entries) == 2
label_registry.async_delete(label1.label_id)
entries = er.async_entries_for_label(entity_registry, label1.label_id)
assert len(entries) == 0
entries = er.async_entries_for_label(entity_registry, label2.label_id)
assert len(entries) == 2
label_registry.async_delete(label2.label_id)
entries = er.async_entries_for_label(entity_registry, label1.label_id)
assert len(entries) == 0
entries = er.async_entries_for_label(entity_registry, label2.label_id)
assert len(entries) == 0

View file

@ -233,6 +233,88 @@ def area_mock(hass):
)
@pytest.fixture
def label_mock(hass: HomeAssistant) -> None:
"""Mock including label info."""
hass.states.async_set("light.Bowl", STATE_ON)
hass.states.async_set("light.Ceiling", STATE_OFF)
hass.states.async_set("light.Kitchen", STATE_OFF)
device_has_label1 = dr.DeviceEntry(labels={"label1"})
device_has_label2 = dr.DeviceEntry(labels={"label2"})
device_has_labels = dr.DeviceEntry(labels={"label1", "label2"})
device_no_labels = dr.DeviceEntry(id="device-no-labels")
mock_device_registry(
hass,
{
device_has_label1.id: device_has_label1,
device_has_label2.id: device_has_label2,
device_has_labels.id: device_has_labels,
device_no_labels.id: device_no_labels,
},
)
entity_with_my_label = er.RegistryEntry(
entity_id="light.with_my_label",
unique_id="with_my_label",
platform="test",
labels={"my-label"},
)
hidden_entity_with_my_label = er.RegistryEntry(
entity_id="light.hidden_with_my_label",
unique_id="hidden_with_my_label",
platform="test",
labels={"my-label"},
hidden_by=er.RegistryEntryHider.USER,
)
config_entity_with_my_label = er.RegistryEntry(
entity_id="light.config_with_my_label",
unique_id="config_with_my_label",
platform="test",
labels={"my-label"},
entity_category=EntityCategory.CONFIG,
)
entity_with_label1_from_device = er.RegistryEntry(
entity_id="light.with_label1_from_device",
unique_id="with_label1_from_device",
platform="test",
device_id=device_has_label1.id,
)
entity_with_label1_and_label2_from_device = er.RegistryEntry(
entity_id="light.with_label1_and_label2_from_device",
unique_id="with_label1_and_label2_from_device",
platform="test",
labels={"label1"},
device_id=device_has_label2.id,
)
entity_with_labels_from_device = er.RegistryEntry(
entity_id="light.with_labels_from_device",
unique_id="with_labels_from_device",
platform="test",
device_id=device_has_labels.id,
)
entity_with_no_labels = er.RegistryEntry(
entity_id="light.no_labels",
unique_id="no_labels",
platform="test",
device_id=device_no_labels.id,
)
mock_registry(
hass,
{
config_entity_with_my_label.entity_id: config_entity_with_my_label,
entity_with_label1_and_label2_from_device.entity_id: entity_with_label1_and_label2_from_device,
entity_with_label1_from_device.entity_id: entity_with_label1_from_device,
entity_with_labels_from_device.entity_id: entity_with_labels_from_device,
entity_with_my_label.entity_id: entity_with_my_label,
entity_with_no_labels.entity_id: entity_with_no_labels,
hidden_entity_with_my_label.entity_id: hidden_entity_with_my_label,
},
)
async def test_call_from_config(hass: HomeAssistant) -> None:
"""Test the sync wrapper of service.async_call_from_config."""
calls = async_mock_service(hass, "test_domain", "test_service")
@ -559,6 +641,39 @@ async def test_extract_entity_ids_from_devices(hass: HomeAssistant, area_mock) -
)
async def test_extract_entity_ids_from_labels(
hass: HomeAssistant, label_mock: None
) -> None:
"""Test extract_entity_ids method with labels."""
call = ServiceCall("light", "turn_on", {"label_id": "my-label"})
assert {
"light.with_my_label",
} == await service.async_extract_entity_ids(hass, call)
call = ServiceCall("light", "turn_on", {"label_id": "label1"})
assert {
"light.with_label1_from_device",
"light.with_labels_from_device",
"light.with_label1_and_label2_from_device",
} == await service.async_extract_entity_ids(hass, call)
call = ServiceCall("light", "turn_on", {"label_id": ["label2"]})
assert {
"light.with_labels_from_device",
"light.with_label1_and_label2_from_device",
} == await service.async_extract_entity_ids(hass, call)
assert (
await service.async_extract_entity_ids(
hass, ServiceCall("light", "turn_on", {"label_id": ENTITY_MATCH_NONE})
)
== set()
)
async def test_async_get_all_descriptions(hass: HomeAssistant) -> None:
"""Test async_get_all_descriptions."""
group = hass.components.group
@ -1500,6 +1615,45 @@ async def test_extract_from_service_area_id(hass: HomeAssistant, area_mock) -> N
]
async def test_extract_from_service_label_id(
hass: HomeAssistant, label_mock: None
) -> None:
"""Test the extraction using label ID as reference."""
entities = [
MockEntity(name="with_my_label", entity_id="light.with_my_label"),
MockEntity(name="no_labels", entity_id="light.no_labels"),
MockEntity(
name="with_labels_from_device", entity_id="light.with_labels_from_device"
),
]
call = ServiceCall("light", "turn_on", {"label_id": "my-label"})
extracted = await service.async_extract_entities(hass, entities, call)
assert len(extracted) == 1
assert extracted[0].entity_id == "light.with_my_label"
call = ServiceCall("light", "turn_on", {"label_id": ["my-label", "label1"]})
extracted = await service.async_extract_entities(hass, entities, call)
assert len(extracted) == 2
assert sorted(ent.entity_id for ent in extracted) == [
"light.with_labels_from_device",
"light.with_my_label",
]
call = ServiceCall(
"light",
"turn_on",
{"label_id": ["my-label", "label1"], "device_id": "device-no-labels"},
)
extracted = await service.async_extract_entities(hass, entities, call)
assert len(extracted) == 3
assert sorted(ent.entity_id for ent in extracted) == [
"light.no_labels",
"light.with_labels_from_device",
"light.with_my_label",
]
async def test_entity_service_call_warn_referenced(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
@ -1511,12 +1665,14 @@ async def test_entity_service_call_warn_referenced(
"area_id": "non-existent-area",
"entity_id": "non.existent",
"device_id": "non-existent-device",
"label_id": "non-existent-label",
},
)
await service.entity_service_call(hass, {}, "", call)
assert (
"Referenced areas non-existent-area, devices non-existent-device, "
"entities non.existent are missing or not currently available"
"entities non.existent, labels non-existent-label "
"are missing or not currently available"
) in caplog.text
@ -1531,13 +1687,15 @@ async def test_async_extract_entities_warn_referenced(
"area_id": "non-existent-area",
"entity_id": "non.existent",
"device_id": "non-existent-device",
"label_id": "non-existent-label",
},
)
extracted = await service.async_extract_entities(hass, {}, call)
assert len(extracted) == 0
assert (
"Referenced areas non-existent-area, devices non-existent-device, "
"entities non.existent are missing or not currently available"
"entities non.existent, labels non-existent-label "
"are missing or not currently available"
) in caplog.text

View file

@ -37,6 +37,7 @@ from homeassistant.helpers import (
device_registry as dr,
entity,
entity_registry as er,
label_registry as lr,
template,
)
from homeassistant.helpers.entity_platform import EntityPlatform
@ -4953,3 +4954,290 @@ async def test_lru_increases_with_many_entities(hass: HomeAssistant) -> None:
assert template.CACHED_TEMPLATE_NO_COLLECT_LRU.get_size() == int(
round(mock_entity_count * template.ENTITY_COUNT_GROWTH_FACTOR)
)
async def test_labels(
hass: HomeAssistant,
label_registry: lr.LabelRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test labels function."""
# Test no labels
info = render_to_info(hass, "{{ labels() }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test one label
label1 = label_registry.async_get_or_create("label1")
info = render_to_info(hass, "{{ labels() }}")
assert_result_info(info, [label1.label_id])
assert info.rate_limit is None
# Test multiple label
label2 = label_registry.async_get_or_create("label2")
info = render_to_info(hass, "{{ labels() }}")
assert_result_info(info, [label1.label_id, label2.label_id])
assert info.rate_limit is None
# Test non-exsting entity ID
info = render_to_info(hass, "{{ labels('sensor.fake') }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'sensor.fake' | labels }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test non existing device ID (hex value)
info = render_to_info(hass, "{{ labels('123abc') }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ '123abc' | labels }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Create a device & entity for testing
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
# Test entity, which has no labels
info = render_to_info(hass, f"{{{{ labels('{entity_entry.entity_id}') }}}}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{entity_entry.entity_id}' | labels }}}}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test device, which has no labels
info = render_to_info(hass, f"{{{{ labels('{device_entry.id}') }}}}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{device_entry.id}' | labels }}}}")
assert_result_info(info, [])
assert info.rate_limit is None
# Add labels to the entity & device
label1 = label_registry.async_get_or_create("label1")
label2 = label_registry.async_get_or_create("label2")
device_entry = device_registry.async_update_device(
device_entry.id, labels=[label1.label_id]
)
entity_entry = entity_registry.async_update_entity(
entity_entry.entity_id, labels=[label2.label_id]
)
# Test entity, which now has a label
info = render_to_info(hass, f"{{{{ '{entity_entry.entity_id}' | labels }}}}")
assert_result_info(info, [label2.label_id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ labels('{entity_entry.entity_id}') }}}}")
assert_result_info(info, [label2.label_id])
assert info.rate_limit is None
# Test device, which now has a label
info = render_to_info(hass, f"{{{{ '{device_entry.id}' | labels }}}}")
assert_result_info(info, [label1.label_id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ labels('{device_entry.id}') }}}}")
assert_result_info(info, [label1.label_id])
assert info.rate_limit is None
async def test_label_id(
hass: HomeAssistant,
label_registry: lr.LabelRegistry,
) -> None:
"""Test label_id function."""
# Test non existing label name
info = render_to_info(hass, "{{ label_id('non-existing label') }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'non-existing label' | label_id }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ label_id(42) }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | label_id }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test with an actual label
label = label_registry.async_get_or_create("existing label")
info = render_to_info(hass, "{{ label_id('existing label') }}")
assert_result_info(info, label.label_id)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'existing label' | label_id }}")
assert_result_info(info, label.label_id)
assert info.rate_limit is None
async def test_label_name(
hass: HomeAssistant,
label_registry: lr.LabelRegistry,
) -> None:
"""Test label_name function."""
# Test non existing label ID
info = render_to_info(hass, "{{ label_name('1234567890') }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ '1234567890' | label_name }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ label_name(42) }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | label_name }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existing label ID
label = label_registry.async_get_or_create("choo choo")
info = render_to_info(hass, f"{{{{ label_name('{label.label_id}') }}}}")
assert_result_info(info, label.name)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.label_id}' | label_name }}}}")
assert_result_info(info, label.name)
assert info.rate_limit is None
async def test_label_entities(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
label_registry: lr.LabelRegistry,
) -> None:
"""Test label_entities function."""
# Test non existing device id
info = render_to_info(hass, "{{ label_entities('deadbeef') }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'deadbeef' | label_entities }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ label_entities(42) }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | label_entities }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Create a fake config entry with a entity
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
entity_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
)
# Add a label to the entity
label = label_registry.async_get_or_create("Romantic Lights")
entity_registry.async_update_entity(entity_entry.entity_id, labels={label.label_id})
# Get entities by label ID
info = render_to_info(hass, f"{{{{ label_entities('{label.label_id}') }}}}")
assert_result_info(info, ["light.hue_5678"])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.label_id}' | label_entities }}}}")
assert_result_info(info, ["light.hue_5678"])
assert info.rate_limit is None
# Get entities by label name
info = render_to_info(hass, f"{{{{ label_entities('{label.name}') }}}}")
assert_result_info(info, ["light.hue_5678"])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.name}' | label_entities }}}}")
assert_result_info(info, ["light.hue_5678"])
assert info.rate_limit is None
async def test_label_devices(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
label_registry: ar.AreaRegistry,
) -> None:
"""Test label_devices function."""
# Test non existing device ID
info = render_to_info(hass, "{{ label_devices('deadbeef') }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'deadbeef' | label_devices }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ label_devices(42) }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | label_devices }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Create a fake config entry with a device
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
# Add a label to it
label = label_registry.async_get_or_create("Romantic Lights")
device_registry.async_update_device(device_entry.id, labels=[label.label_id])
# Get the devices from a label by its ID
info = render_to_info(hass, f"{{{{ label_devices('{label.label_id}') }}}}")
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.label_id}' | label_devices }}}}")
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None
# Get the devices from a label by its name
info = render_to_info(hass, f"{{{{ label_devices('{label.name}') }}}}")
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.name}' | label_devices }}}}")
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None

View file

@ -29,6 +29,7 @@ from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
label_registry as lr,
)
@ -60,6 +61,10 @@ class EntityRegistryEntrySnapshot(dict):
"""Tiny wrapper to represent an entity registry entry in snapshots."""
class LabelRegistryEntrySnapshot(dict):
"""Tiny wrapper to represent an label registry entry in snapshots."""
class FlowResultSnapshot(dict):
"""Tiny wrapper to represent a flow result in snapshots."""
@ -104,6 +109,8 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
serializable_data = cls._serializable_entity_registry_entry(data)
elif isinstance(data, ir.IssueEntry):
serializable_data = cls._serializable_issue_registry_entry(data)
elif isinstance(data, lr.LabelEntry):
serializable_data = cls._serializable_issue_registry_entry(data)
elif isinstance(data, dict) and "flow_id" in data and "handler" in data:
serializable_data = cls._serializable_flow_result(data)
elif isinstance(data, vol.Schema):
@ -187,6 +194,13 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
"""Prepare a Home Assistant issue registry entry for serialization."""
return IssueRegistryItemSnapshot(data.to_json() | {"created": ANY})
@classmethod
def _serializable_label_registry_entry(
cls, data: lr.LabelEntry
) -> SerializableData:
"""Prepare a Home Assistant label registry entry for serialization."""
return LabelRegistryEntrySnapshot(dataclasses.asdict(data))
@classmethod
def _serializable_state(cls, data: State) -> SerializableData:
"""Prepare a Home Assistant State for serialization."""