This commit is contained in:
Home Assistant Version Control
2026-09-04 13:37:39 +00:00
parent 70db0a842d
commit 1f0e4d0959
6 changed files with 234 additions and 124 deletions
+6 -5
View File
@@ -25,7 +25,7 @@ DOMAIN = "ha_mcp_tools"
# in CI. The
# capability negotiation — not this version — gates each WS command (see
# ``websocket_api.CAPABILITIES``).
COMPONENT_VERSION = "2.1.1"
COMPONENT_VERSION = "2.1.2"
# Config-entry discriminator (``entry.data[CONF_ENTRY_TYPE]``). A missing value
# means "tools" so the pre-existing services entry keeps working across the
@@ -471,10 +471,11 @@ DATA_LLM_API_UNSUB = "llm_api_unsub"
WEBHOOK_AUTH_NONE = "none" # secret webhook URL is the shared secret (default)
WEBHOOK_AUTH_HA = "ha_auth" # HA-native bearer (HA core is the OAuth AS)
# Self-hosted OAuth 2.1 authorization server with a static client_id/secret,
# ported from the webhook-proxy add-on's "legacy" mode. Needed because HA
# core's /auth/authorize does not yet fetch Client ID Metadata Documents for
# cross-origin redirect_uris (home-assistant/core#176282), which is what
# Google Gemini Spark's custom connected apps require.
# ported from the webhook-proxy add-on's "legacy" mode. Originally needed
# because HA core's /auth/authorize did not fetch Client ID Metadata Documents
# for cross-origin redirect_uris before 2026.9 (home-assistant/core#176282,
# fixed by #176286), which Google Gemini Spark's custom connected apps require;
# kept as the pasted-credential fallback for clients that want one.
WEBHOOK_AUTH_LEGACY = "legacy"
# Default bind host + port. 9584 (not the add-on's 9583) so this in-process
+1 -1
View File
@@ -22,5 +22,5 @@
"requirements": [
"ruamel.yaml>=0.18.0"
],
"version": "2.1.1"
"version": "2.1.2"
}
@@ -4,10 +4,11 @@ Ported from the webhook-proxy add-on's ``mcp_proxy/oauth.py`` ``OAuthProvider``
+ ``AuthorizeView`` + ``TokenView`` (the proven ``legacy`` mode). Self-hosted,
single-tenant authorization server with a static client_id/client_secret pair,
for MCP clients that need a credential to paste rather than HA core's native
OAuth (``ha_auth``) — currently just Google Gemini Spark, whose custom
connected apps use the Client ID Metadata Document pattern that HA core's
``/auth/authorize`` does not yet support for cross-origin redirect_uris
(home-assistant/core#176282).
OAuth (``ha_auth``). Written for Google Gemini Spark, whose custom connected
apps use the Client ID Metadata Document pattern that HA core's
``/auth/authorize`` did not support for cross-origin redirect_uris before
2026.9 (home-assistant/core#176282, fixed by #176286); ``ha_auth`` validates
CIMD itself since 2.0.0, so this is now the pasted-credential fallback.
Unlike the add-on, this module holds no reference to ``hass``, the webhook id,
or a public base URL: the discovery-document layer (RFC 8414 / RFC 9728) and
+203 -95
View File
@@ -2,9 +2,10 @@
This module registers versioned ``ha_mcp_tools/*`` WebSocket commands that the
ha-mcp server calls in-process (same HA core, no REST/WS round-trips) behind a
capability gate. It registers twenty-three commands. It advertises twenty-five
capabilities: twenty-two command capabilities plus three additive flags
(dashboards_doc_search, search_visibility, and search_entity_membership);
capability gate. It registers twenty-three commands. It advertises twenty-six
capabilities: twenty-two command capabilities plus four additive flags
(dashboards_doc_search, search_visibility, search_entity_membership, and
search_visibility_allowlist_authorization);
the info handshake carries no capability entry:
* ``ha_mcp_tools/info`` — the handshake: ``schema_version`` + ``capabilities[]``
@@ -111,10 +112,17 @@ the info handshake carries no capability entry:
:func:`_visibility_hidden_set` mirrors the server's ``hidden_entity_ids``, with
the Assist dimension delegated to core's ``async_should_expose``), so
``ha_search`` can route through the component even with an active filter. A
degraded dimension (unknown ``exclude_category`` / empty-registry allowlist /
unavailable Assist) fails open, and :func:`_visibility_warnings` returns the
resolver-parity ``visibility_warnings`` the response carries so the filtering is
not silently incomplete.
second ``search_visibility_allowlist_authorization`` capability declares that
this component understands the ``allowlist_authorization`` wire key, which the
server sends only to a component advertising it and which selects the revised
precedence (an allowlist match authorizes past category, HA-hidden, and Assist
filters). Without the key the component keeps the legacy conjunctive
precedence, so it stays correct against an older released server that still
resolves the old way in its own outbound scan. A degraded dimension (unknown
``exclude_category`` / empty-registry allowlist / unavailable Assist) fails
open, and :func:`_visibility_warnings` returns the resolver-parity
``visibility_warnings`` carried by the response so filtering is not silently
incomplete.
* ``ha_mcp_tools/server_entry`` — the component locates its OWN server config
entry (``entry.data[CONF_ENTRY_TYPE] == server``, the one marker key it reads
from ``entry.data``) and returns ``{entry_id, channel, pip_spec}`` (channel /
@@ -268,7 +276,7 @@ from collections.abc import Collection, Mapping
from dataclasses import dataclass
from difflib import SequenceMatcher
from pathlib import Path
from typing import Any
from typing import Any, NamedTuple
import voluptuous as vol
import yaml # type: ignore[import-untyped]
@@ -376,6 +384,11 @@ CAPABILITIES: list[str] = [
# would ignore the param is never sent it (param-sniffing is banned for
# routing; the CAPABILITIES flag is what the server gates on).
"search_visibility",
# A semantic flag on search_visibility: this component understands the
# ``allowlist_authorization`` wire key (revised allowlist precedence). The
# server sends that key only to a component advertising this flag, and with
# an active allowlist requires the flag or falls back to its legacy resolver.
"search_visibility_allowlist_authorization",
"server_entry",
# The WRITE counterpart of ``server_entry`` (Phase 3). The server gates its
# ``ha_dev_manage_server(update_source)`` embedded-mode direct-write on this; an
@@ -644,10 +657,13 @@ def _info_schema() -> dict[Any, Any]:
# The nine hide dimensions ``VisibilityConfig.to_wire`` emits, split by wire type
# (seven id/name lists, two bool flags). Kept in lockstep with the server's
# ``to_wire`` and :func:`_visibility_hidden_set`; a new dimension is a new component
# capability, added on BOTH sides (see :func:`_visibility_param_schema`). The union
# is pinned equal to the server resolver's key set by the cross-seam contract test.
# (seven id/name lists, two bool flags), plus the optional
# ``allowlist_authorization`` precedence flag the server adds for a component that
# advertises ``search_visibility_allowlist_authorization``. Kept in lockstep with
# the server's ``to_wire`` / ``VisibilityWire`` and :func:`_visibility_hidden_set`;
# a new dimension is a new component capability, added on BOTH sides (see
# :func:`_visibility_param_schema`). The union is pinned equal to the server
# resolver's key set by the cross-seam contract test.
_VISIBILITY_LIST_KEYS = (
"exclude_categories",
"deny_entity_ids",
@@ -657,18 +673,25 @@ _VISIBILITY_LIST_KEYS = (
"allow_areas",
"allow_labels",
)
_VISIBILITY_BOOL_KEYS = ("exclude_hidden", "respect_assist_exposure")
_VISIBILITY_BOOL_KEYS = (
"exclude_hidden",
"respect_assist_exposure",
"allowlist_authorization",
)
def _visibility_param_schema() -> Any:
"""Voluptuous schema for the ``search`` ``visibility`` dict — exactly the nine keys.
"""Voluptuous schema for the ``search`` ``visibility`` dict — exactly the ten keys.
Enumerating the known dimensions (PREVENT_EXTRA is voluptuous' default for a
nested ``Schema``) makes an unknown key a loud ``invalid_format`` command error
rather than a silent drop. If a newer server emits a tenth dimension to this
1.2.0 component, the server's error taxonomy converts that into a legacy fallback
with the filter STILL correctly applied — structural fail-closed for free —
instead of partial, unwarned filtering. Built at call time so it honors the
The nine hide dimensions plus the ``allowlist_authorization`` precedence flag
(sent only when this component advertises
``search_visibility_allowlist_authorization``; absent means legacy
precedence). Enumerating the known keys (PREVENT_EXTRA is voluptuous' default
for a nested ``Schema``) makes an unknown key a loud ``invalid_format`` command
error rather than a silent drop. If a newer server emits an eleventh key to
this component, the server's error taxonomy converts that into a legacy
fallback with the filter STILL correctly applied — structural fail-closed for
free — instead of partial, unwarned filtering. Built at call time so it honors the
monkeypatched ``vol`` in the unit suite.
"""
schema: dict[Any, Any] = {vol.Optional(key): [str] for key in _VISIBILITY_LIST_KEYS}
@@ -693,15 +716,13 @@ def _search_schema() -> dict[Any, Any]:
int, vol.Range(min=1, max=MAX_RESULTS)
),
vol.Optional("offset", default=0): vol.All(int, vol.Range(min=0)),
# Opt-in entity-visibility filter (``search_visibility`` capability): the
# server's raw VisibilityConfig hide dimensions. When present and
# non-empty, hidden entities are excluded from the entity results before
# counts/pagination, mirroring the legacy ``load_hidden_set`` hard-exclude
# so ``ha_search`` can drop its "filter active -> legacy only" gate. Gated
# by the CAPABILITIES flag, so an old component never receives it. The nine
# known keys are enumerated so an unknown dimension fails loudly (the server
# then falls back to legacy with the filter applied) — see
# :func:`_visibility_param_schema`.
# Opt-in entity visibility for component search. The component advertises
# ``search_visibility`` and ``search_visibility_allowlist_authorization``;
# the server reads those flags before sending these raw VisibilityConfig
# dimensions, and adds ``allowlist_authorization`` only for the second flag.
# The ten known keys are enumerated so an unknown one fails loudly and the
# server falls back to its own filtered legacy path.
# See ``_visibility_param_schema``.
vol.Optional("visibility"): _visibility_param_schema(),
}
@@ -1110,23 +1131,30 @@ def _do_search(
# :func:`_visibility_hidden_set`.
if isinstance(visibility, Mapping) and visibility:
states_list = _iter_states(hass)
# Probe Assist availability once (only when the config asks for it) so a
# requested-but-unavailable Assist dimension both skips its hiding and
# surfaces the resolver-parity degradation warning.
assist_available = (
_assist_exposure_available(hass)
if visibility.get("respect_assist_exposure")
else True
inventory = _visibility_inventory(view, states_list, visibility)
assist_applies = (
bool(visibility.get("respect_assist_exposure"))
and not inventory.allowlist.authorized
)
# Short-circuit the probe unless Assist applies. True on the skipped
# path means there is no Assist degradation to report; the hidden-set
# helper does not consult Assist while an authorizing allowlist is
# active (a legacy wire keeps Assist in play, so the probe still runs).
assist_available = not assist_applies or _assist_exposure_available(hass)
hidden = _visibility_hidden_set(
view,
states_list,
visibility,
lambda eid: _assist_should_expose(hass, eid),
assist_available=assist_available,
inventory=inventory,
)
visibility_warnings = _visibility_warnings(
view, states_list, visibility, assist_available=assist_available
view,
states_list,
visibility,
assist_available=assist_available,
inventory=inventory,
)
if hidden:
scored_entities = [
@@ -2614,6 +2642,7 @@ def _all_entity_entries(view: _RegistryView) -> list[Any]:
try:
return list(entities.values())
except Exception: # pragma: no cover - defensive
_LOGGER.warning("entity registry enumeration failed", exc_info=True)
return []
@@ -5001,6 +5030,67 @@ _ALLOWLIST_REGISTRY_EMPTY_WARNING = (
)
class _AllowlistState(NamedTuple):
"""Effective restrict-mode activity, registry degradation, and precedence.
``authorized`` is ``active`` plus the ``allowlist_authorization`` wire flag:
an allow match then authorizes the entity past the category, HA-hidden, and
Assist filters. Without the flag the legacy conjunctive semantics apply even
while ``active`` is True (an older server resolves that way itself).
"""
active: bool
degraded: bool
authorized: bool
class _VisibilityInventory(NamedTuple):
"""Registry/state indexes and allowlist state shared by one search."""
registry_by_id: dict[str, Any]
state_ids: set[str]
allowlist: _AllowlistState
def _visibility_allowlist_state(
registry_by_id: Mapping[str, Any],
state_ids: set[str],
visibility: Mapping[str, Any],
) -> _AllowlistState:
"""Resolve allowlist activity from one precomputed search inventory.
``degraded`` deliberately remains true when an explicit ``allow_entity_ids``
list keeps restrict mode active: only the registry-derived allow dimensions
degraded, so callers still warn while the explicit ID allowlist remains in
force. This differs from the resolver's pre-fetch predicate, which asks only
whether degradation would make Assist relevant again.
``authorized`` additionally requires the ``allowlist_authorization`` wire flag:
an active allowlist alone keeps the legacy conjunctive precedence.
"""
entity_ids_active = bool(visibility.get("allow_entity_ids"))
registry_dimensions_active = bool(
visibility.get("allow_areas") or visibility.get("allow_labels")
)
degraded = registry_dimensions_active and not registry_by_id and bool(state_ids)
active = entity_ids_active or (registry_dimensions_active and not degraded)
authorized = active and bool(visibility.get("allowlist_authorization"))
return _AllowlistState(active, degraded, authorized)
def _visibility_inventory(
view: _RegistryView, states: Any, visibility: Mapping[str, Any]
) -> _VisibilityInventory:
"""Build the registry/state inventory once for one visibility computation."""
registry_by_id = _registry_index_by_id(view)
state_ids = _state_entity_ids(states)
return _VisibilityInventory(
registry_by_id,
state_ids,
_visibility_allowlist_state(registry_by_id, state_ids, visibility),
)
def _unknown_categories_warning(unknown_categories: set[str]) -> str:
"""The resolver's unknown-``exclude_categories`` warning text (byte-identical)."""
return (
@@ -5016,14 +5106,17 @@ def _visibility_hidden_set(
should_expose_fn: Any,
*,
assist_available: bool = True,
inventory: _VisibilityInventory | None = None,
) -> set[str]:
"""Compute the opt-in hidden entity_id set, mirroring the server's resolver.
A pure replication of ``visibility.resolver.hidden_entity_ids`` over the live
``_RegistryView`` + ``states`` (rather than the WS ``{success, result}``
payloads the server passes): a conjunction of independent hide dimensions —
the deny list, the category / hidden / area / label excludes (area/labels are
device-inherited), the allow-list restrict mode, and the Assist dimension. The
payloads the server passes), including its precedence: concrete deny and
area/label excludes win; an allowlist match authorizes past the broad category,
Home Assistant hidden-state, and Assist filters when the wire opted into that
revised precedence with ``allowlist_authorization`` (without the flag those
filters still apply to a matched entity, matching the older server). The
Assist dimension delegates to the injectable ``should_expose_fn(entity_id) ->
bool`` (:func:`_assist_should_expose` in production — a READ-ONLY reconstruction
of core's ``async_should_expose`` from the explicit exposure map + expose_new +
@@ -5033,11 +5126,11 @@ def _visibility_hidden_set(
computed defaults back, which this fast path must not do (see
:func:`_assist_should_expose`).
``should_expose_fn`` is consulted only when ``respect_assist_exposure`` is set
AND ``assist_available`` is True. When the config requests the Assist dimension
but the exposure machinery is unavailable (``assist_available=False``), the
dimension is SKIPPED — hiding nothing by Assist — mirroring the resolver's
"skip the dimension when its data is unavailable" fail-open (the paired
``should_expose_fn`` is consulted only when ``respect_assist_exposure`` is set,
no AUTHORIZING allowlist is active, and ``assist_available`` is True. When the config
requests the Assist dimension but the exposure machinery is unavailable
(``assist_available=False``), the dimension is SKIPPED — hiding nothing by
Assist — mirroring the resolver's fail-open behavior (the paired
degradation warning is surfaced by :func:`_visibility_warnings`). Kept a
standalone pure function so it is unit-testable without the full search
pipeline.
@@ -5052,29 +5145,25 @@ def _visibility_hidden_set(
allow_areas = set(visibility.get("allow_areas") or [])
allow_labels = set(visibility.get("allow_labels") or [])
respect_assist = bool(visibility.get("respect_assist_exposure"))
# ``assist_active`` gates the per-entity Assist should_expose sub-check (skipped
# when the config asks for Assist but its data is unavailable). The allow/assist
# loop below is guarded by ``allow_active or respect_assist`` — a structural
# mirror of the resolver's guard, not a functional requirement: when Assist is
# requested-but-unavailable and no allowlist is set, the loop runs but hides
# nothing (both sub-checks are inert). Kept identical so the two paths match.
assist_active = respect_assist and assist_available
allow_active = bool(allow_areas or allow_labels or allow_entity_ids)
registry_by_id = _registry_index_by_id(view)
if inventory is None:
inventory = _visibility_inventory(view, states, visibility)
registry_by_id = inventory.registry_by_id
# states-only entity universe (YAML/template entities absent from the registry
# that the allow / Assist dimensions must still be able to hide).
state_ids = {
eid for eid in (getattr(s, "entity_id", None) for s in states or []) if eid
}
# Fail-open guard (mirrors the resolver): an area/label allowlist needs registry
# data to match; if the registry is empty but there are states-only candidates,
# restrict mode would blank everything, so drop those allow dimensions.
if (allow_areas or allow_labels) and not registry_by_id and state_ids:
state_ids = inventory.state_ids
allow_active = inventory.allowlist.active
authorized = inventory.allowlist.authorized
# Fail-open guard: registry-derived allow dimensions cannot match when the
# registry is empty but states-only candidates exist.
if inventory.allowlist.degraded:
allow_areas = set()
allow_labels = set()
allow_active = bool(allow_entity_ids)
# An authorizing allowlist skips the broad Assist filter; a legacy wire keeps
# it. Only a full degradation (registry-derived dimensions dropped and no
# allow_entity_ids left) makes ``active`` False and re-enables Assist.
assist_active = respect_assist and assist_available and not authorized
hidden: set[str] = set(denied)
_apply_visibility_excludes(
@@ -5086,8 +5175,9 @@ def _visibility_hidden_set(
exclude_areas,
exclude_labels,
hidden,
automatic_excludes_active=not authorized,
)
if allow_active or respect_assist:
if allow_active or assist_active:
_apply_visibility_allow_assist(
view,
registry_by_id,
@@ -5099,6 +5189,7 @@ def _visibility_hidden_set(
assist_active,
should_expose_fn,
hidden,
allow_authorizes=authorized,
)
return hidden
@@ -5109,6 +5200,7 @@ def _visibility_warnings(
visibility: Mapping[str, Any],
*,
assist_available: bool = True,
inventory: _VisibilityInventory | None = None,
) -> list[str]:
"""Degradation warnings for a visibility computation, mirroring the resolver.
@@ -5133,19 +5225,16 @@ def _visibility_warnings(
if unknown:
warnings.append(_unknown_categories_warning(unknown))
allow_areas = set(visibility.get("allow_areas") or [])
allow_labels = set(visibility.get("allow_labels") or [])
registry_by_id = _registry_index_by_id(view)
state_ids = {
eid for eid in (getattr(s, "entity_id", None) for s in states or []) if eid
}
# Empty-registry allowlist fail-open: same guard as _visibility_hidden_set —
# only fires when there are states-only candidates the restrict mode would
# otherwise blank.
if (allow_areas or allow_labels) and not registry_by_id and state_ids:
if inventory is None:
inventory = _visibility_inventory(view, states, visibility)
if inventory.allowlist.degraded:
warnings.append(_ALLOWLIST_REGISTRY_EMPTY_WARNING)
if bool(visibility.get("respect_assist_exposure")) and not assist_available:
if (
visibility.get("respect_assist_exposure")
and not inventory.allowlist.authorized
and not assist_available
):
warnings.append(_ASSIST_UNAVAILABLE_WARNING)
return warnings
@@ -5161,6 +5250,15 @@ def _registry_index_by_id(view: _RegistryView) -> dict[str, Any]:
return index
def _state_entity_ids(states: Any) -> set[str]:
"""Entity IDs present in the live state-machine snapshot."""
return {
eid
for eid in (getattr(state, "entity_id", None) for state in states or [])
if eid
}
def _apply_visibility_excludes(
view: _RegistryView,
registry_by_id: dict[str, Any],
@@ -5170,22 +5268,27 @@ def _apply_visibility_excludes(
exclude_areas: set[str],
exclude_labels: set[str],
hidden: set[str],
*,
automatic_excludes_active: bool,
) -> None:
"""Add the exclude-dimension hits to ``hidden`` (registry-derived, deny-first).
"""Add automatic and hard exclude hits to ``hidden``.
Mirrors the resolver's exclude loop. The empty-set dimensions are inert (``x in
set()`` / ``set() & x`` are falsy), so an inactive dimension hides nothing
without a guard; only ``exclude_hidden`` is a bool flag and keeps its guard.
Category/HA-hidden filters are skipped for an AUTHORIZING allowlist; concrete
area/label exclusions remain hard conflicts and always apply. The empty-set
dimensions are inert (``x in set()`` / ``set() & x`` are falsy), so an inactive
dimension hides nothing without a guard; only ``exclude_hidden`` is a bool flag
and keeps its guard.
"""
for eid, entry in registry_by_id.items():
if eid in denied:
continue
if _enum_value(getattr(entry, "entity_category", None)) in categories:
hidden.add(eid)
continue
if exclude_hidden and getattr(entry, "hidden_by", None) is not None:
hidden.add(eid)
continue
if automatic_excludes_active:
if _enum_value(getattr(entry, "entity_category", None)) in categories:
hidden.add(eid)
continue
if exclude_hidden and getattr(entry, "hidden_by", None) is not None:
hidden.add(eid)
continue
if _effective_area_for_entry(view, entry) in exclude_areas:
hidden.add(eid)
continue
@@ -5204,24 +5307,28 @@ def _apply_visibility_allow_assist(
assist_active: bool,
should_expose_fn: Any,
hidden: set[str],
*,
allow_authorizes: bool,
) -> None:
"""Add allow-restrict + Assist hits to ``hidden`` over registry + states.
These conjunctive filters must reach states-only entities, so they iterate the
full candidate universe. Mirrors the resolver's allow/assist loop. ``should_expose_fn``
is consulted only when ``assist_active`` (Assist requested AND its data
available); an unavailable-Assist request degrades to no Assist hiding here,
with the warning surfaced separately.
Both filters reach states-only entities. A nonmatching entity is always hidden
while the allowlist is active. With ``allow_authorizes`` a match skips the
Assist check; on a legacy wire a matched entity still faces it, so
``should_expose_fn`` is consulted whenever Assist is requested and available.
"""
for eid in registry_by_id.keys() | state_ids:
if eid in hidden:
continue
entry = registry_by_id.get(eid)
if allow_active and not _entity_allowed(
view, eid, entry, allow_entity_ids, allow_areas, allow_labels
):
hidden.add(eid)
continue
if allow_active:
if not _entity_allowed(
view, eid, entry, allow_entity_ids, allow_areas, allow_labels
):
hidden.add(eid)
continue
if allow_authorizes:
continue
if assist_active and not should_expose_fn(eid):
hidden.add(eid)
@@ -5391,6 +5498,7 @@ def _assist_exposure_available(hass: HomeAssistant) -> bool:
DATA_EXPOSED_ENTITIES,
)
except Exception:
_LOGGER.warning("Assist exposure support import failed", exc_info=True)
return False
data = getattr(hass, "data", None)
return isinstance(data, Mapping) and DATA_EXPOSED_ENTITIES in data
+2 -2
View File
@@ -4,7 +4,7 @@
"version": 1,
"source": "zigbee-herdsman@10.9.1",
"internal": {
"date": "2026-09-03T19:08:55.796Z",
"date": "2026-09-04T13:37:32.664Z",
"znpVersion": 1
}
},
@@ -25,7 +25,7 @@
"network_key": {
"key": "0a7ed3caf6adb653119697df9d04ec25",
"sequence_number": 0,
"frame_counter": 4342097
"frame_counter": 4395150
},
"devices": [
{
+17 -17
View File
@@ -4,10 +4,10 @@
"state": "ON",
"led_brightness": 100,
"countdown_to_turn_off": 0,
"voltage": 118.3,
"voltage": 119.3,
"countdown_to_turn_on": 0,
"ac_frequency": 60,
"power_factor": 0.11,
"power_factor": 0.15,
"update": {
"state": "idle",
"installed_version": 268513381,
@@ -45,7 +45,7 @@
"state": "ON",
"led_brightness": 100,
"countdown_to_turn_off": 0,
"voltage": 118.2,
"voltage": 118.8,
"countdown_to_turn_on": 0,
"energy": 73.98,
"power_factor": 0.2,
@@ -74,13 +74,13 @@
"led_brightness": 100,
"countdown_to_turn_off": 0,
"countdown_to_turn_on": 0,
"voltage": 119.7,
"voltage": 120.8,
"state": "ON",
"ac_frequency": 60,
"energy": 150.75,
"power": 97.9,
"current": 0.86,
"power_factor": 0.95,
"energy": 150.76,
"power": 0.7,
"current": 0.05,
"power_factor": 0.38,
"update": {
"state": "idle",
"installed_version": 268513381,
@@ -95,13 +95,13 @@
"led_brightness": 100,
"countdown_to_turn_off": 0,
"countdown_to_turn_on": 0,
"voltage": 117.8,
"energy": 71.82,
"voltage": 118.8,
"energy": 71.83,
"state": "ON",
"power": 72.2,
"current": 0.8,
"power": 55.5,
"current": 0.64,
"ac_frequency": 60,
"power_factor": 0.75,
"power_factor": 0.77,
"update": {
"state": "idle",
"installed_version": 268513381,
@@ -114,12 +114,12 @@
},
"0xffffb40e060895b3": {
"state": "ON",
"voltage": 118.6,
"voltage": 119.1,
"ac_frequency": 60,
"energy": 10.11,
"current": 0.01,
"power": 0.1,
"power_factor": 0.11,
"power_factor": 0.22,
"linkquality": 98,
"update": {
"state": "idle",
@@ -136,7 +136,7 @@
"0xffffb40e0608864e": {
"led_brightness": 100,
"countdown_to_turn_off": 0,
"voltage": 119.5,
"voltage": 120.1,
"energy": 23.41,
"countdown_to_turn_on": 0,
"state": "ON",
@@ -224,7 +224,7 @@
},
"0xb40e060fffe717c5": {
"battery": 100,
"occupancy": false,
"occupancy": true,
"tamper": false,
"battery_low": false,
"linkquality": 80,