58 files
This commit is contained in:
@@ -705,11 +705,15 @@ Read and modify YAML files to understand and change Home Assistant's defined beh
|
||||
Query and interact with the running Home Assistant instance:
|
||||
- `get_states`, `search_entities`, `get_home_context` - Current entity states and compact area/domain/entity context
|
||||
- `call_service` - Control devices (with confirmation), and read from services that answer with data (`recorder.get_statistics`, `weather.get_forecasts`, `calendar.get_events`, `todo.get_items`) — the response comes back automatically
|
||||
- `get_history`, `get_logbook` - Historical data
|
||||
- `get_history`, `get_logbook` - Historical data; supplied timestamps must include `Z` or a UTC offset
|
||||
- `get_calendar_events` - Calendar events; supplied start/end timestamps must include `Z` or a UTC offset
|
||||
- `get_devices`, `get_areas` - Device and area registry info
|
||||
- `write_config_safe` - **Safe config writing with automatic validation, content protection, and backup**
|
||||
- `validate_config` - Check configuration validity
|
||||
- `get_error_log` - System errors and warnings
|
||||
- `get_supervisor_health`, `get_supervisor_resolution` - Read-only Supervisor, host, connectivity, and repair evidence
|
||||
- `get_backup_posture`, `get_store_audit`, `get_supervisor_metrics` - Bounded backup, software-source, and resource evidence
|
||||
- `get_support_logs` - Bounded, credential-redacted Core, Supervisor, host, or app logs
|
||||
- `diagnose_entity` - Comprehensive entity troubleshooting
|
||||
- `get_agent_capabilities` - OpenCode MCP capabilities and native HA `llm` / MCP readiness
|
||||
- `get_ha_llm_development_guide` - Upstream references and starter template for native `<integration>/llm.py` providers
|
||||
@@ -727,7 +731,7 @@ Query and interact with the running Home Assistant instance:
|
||||
| Control devices | N/A | `call_service` | `hab action call` | N/A |
|
||||
| Read from a service that answers with data | N/A | `call_service` (automatic) | `hab action call --return-response` | N/A |
|
||||
| Add new integrations | Primary | N/A | N/A | N/A |
|
||||
| Troubleshoot issues | Review configs | `diagnose_entity`, `get_error_log` | `hab system health` | N/A |
|
||||
| Troubleshoot issues | Review configs | `diagnose_entity`, `get_error_log`, `get_supervisor_health`, `get_supervisor_resolution` | `hab system health` | N/A |
|
||||
| Check agent/LLM readiness | N/A | `get_agent_capabilities` | N/A | N/A |
|
||||
| Develop native HA LLM tools | `custom_components/*/llm.py` | `get_ha_llm_development_guide` | N/A | N/A |
|
||||
| Find entities | Grep YAML files | `search_entities` | `hab entity list --domain` | N/A |
|
||||
@@ -736,7 +740,7 @@ Query and interact with the running Home Assistant instance:
|
||||
| **Verify UI changes** | N/A | **`screenshot_url`** | N/A | N/A |
|
||||
| **Manage areas/floors** | N/A | `get_areas` (read-only) | **`hab area/floor` (CRUD)** | N/A |
|
||||
| **Manage helpers** | N/A | N/A | **`hab helper` (primary)** | N/A |
|
||||
| **Backups** | N/A | N/A | **`hab backup` (primary)** | N/A |
|
||||
| **Backups** | N/A | `get_backup_posture` | **`hab backup` (primary)** | N/A |
|
||||
| **Blueprints** | N/A | N/A | **`hab blueprint` (primary)** | N/A |
|
||||
| **Update firmware** | N/A | **`watch_firmware_update`** | N/A | N/A |
|
||||
| **Check for updates** | N/A | `get_available_updates` | N/A | N/A |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -212,7 +212,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
global_config = get_global_configuration(hass, config)
|
||||
|
||||
discovery_manager = await create_discovery_manager_instance(hass, config, global_config)
|
||||
discovery_manager = create_discovery_manager_instance(hass, config, global_config)
|
||||
hass.data[DOMAIN] = {
|
||||
DATA_DISCOVERY_MANAGER: discovery_manager,
|
||||
DOMAIN_CONFIG: global_config,
|
||||
@@ -225,6 +225,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
DATA_ANALYTICS: {},
|
||||
}
|
||||
|
||||
await discovery_manager.setup()
|
||||
|
||||
register_services(hass)
|
||||
|
||||
await async_load_platform(hass, Platform.SELECT, DOMAIN, {}, config)
|
||||
@@ -235,8 +237,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
try:
|
||||
await repair_none_config_entries_issue(hass)
|
||||
except Exception as e: # pragma: no cover
|
||||
_LOGGER.error("problem while cleaning up None entities", exc_info=e) # pragma: no cover
|
||||
except Exception: # pragma: no cover
|
||||
_LOGGER.exception("problem while cleaning up None entities") # pragma: no cover
|
||||
|
||||
await init_analytics(hass)
|
||||
|
||||
@@ -272,7 +274,7 @@ async def init_analytics(hass: HomeAssistant) -> None:
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
|
||||
|
||||
|
||||
async def create_discovery_manager_instance(
|
||||
def create_discovery_manager_instance(
|
||||
hass: HomeAssistant,
|
||||
ha_config: ConfigType,
|
||||
global_powercalc_config: ConfigType,
|
||||
@@ -284,15 +286,13 @@ async def create_discovery_manager_instance(
|
||||
exclude_self_usage = discovery_config.get(CONF_EXCLUDE_SELF_USAGE, False)
|
||||
enable_autodiscovery = discovery_config.get(CONF_ENABLED, True)
|
||||
|
||||
manager = DiscoveryManager(
|
||||
return DiscoveryManager(
|
||||
hass,
|
||||
ha_config,
|
||||
exclude_device_types=exclude_device_types,
|
||||
exclude_self_usage_profiles=exclude_self_usage,
|
||||
enabled=enable_autodiscovery,
|
||||
)
|
||||
await manager.setup()
|
||||
return manager
|
||||
|
||||
|
||||
def register_services(hass: HomeAssistant) -> None:
|
||||
@@ -579,8 +579,8 @@ async def repair_none_config_entries_issue(hass: HomeAssistant) -> None:
|
||||
object.__setattr__(entry, "unique_id", unique_id)
|
||||
hass.config_entries._entries._index_entry(entry) # noqa: SLF001
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
except Exception as e: # pragma: no cover
|
||||
_LOGGER.error("problem while cleaning up None entities", exc_info=e) # pragma: no cover
|
||||
except Exception: # pragma: no cover
|
||||
_LOGGER.exception("problem while cleaning up None entities") # pragma: no cover
|
||||
|
||||
|
||||
def _notify_message(
|
||||
|
||||
@@ -38,6 +38,7 @@ from .const import (
|
||||
CONF_POWER,
|
||||
CONF_SENSOR_TYPE,
|
||||
CONF_STANDBY_POWER,
|
||||
DISCOVERY_INTEGRATION_NAME,
|
||||
DISCOVERY_POWER_PROFILES,
|
||||
DISCOVERY_SOURCE_ENTITY,
|
||||
DOMAIN,
|
||||
@@ -400,6 +401,7 @@ class PowercalcConfigFlow(PowercalcCommonFlow, ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self.source_entity_id = self.source_entity.entity_id
|
||||
self.name = self.source_entity.name
|
||||
integration_name = discovery_info.pop(DISCOVERY_INTEGRATION_NAME, None)
|
||||
|
||||
power_profiles: list[PowerProfile] = []
|
||||
if DISCOVERY_POWER_PROFILES in discovery_info:
|
||||
@@ -410,7 +412,7 @@ class PowercalcConfigFlow(PowercalcCommonFlow, ConfigFlow, domain=DOMAIN):
|
||||
self.sensor_config = discovery_info.copy()
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
"name": self.name or "",
|
||||
"name": f"{self.name} - {integration_name}" if integration_name else self.name or "",
|
||||
"manufacturer": str(self.sensor_config.get(CONF_MANUFACTURER)),
|
||||
"model": str(self.sensor_config.get(CONF_MODEL)),
|
||||
}
|
||||
@@ -549,12 +551,7 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow):
|
||||
|
||||
menu = [Step.BASIC_OPTIONS]
|
||||
if self.selected_sensor_type == SensorType.VIRTUAL_POWER:
|
||||
if self.strategy and self.should_add_strategy_option_to_menu():
|
||||
strategy_step = STRATEGY_STEP_MAPPING[self.strategy]
|
||||
menu.append(strategy_step)
|
||||
if self.selected_profile:
|
||||
menu.append(Step.LIBRARY_OPTIONS)
|
||||
menu.append(Step.ADVANCED_OPTIONS)
|
||||
menu.extend(self.build_virtual_power_menu())
|
||||
if self.selected_sensor_type == SensorType.DAILY_ENERGY:
|
||||
menu.append(Step.DAILY_ENERGY)
|
||||
if self.selected_sensor_type == SensorType.REAL_POWER:
|
||||
@@ -570,6 +567,32 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow):
|
||||
|
||||
return menu
|
||||
|
||||
def build_virtual_power_menu(self) -> list[Step]:
|
||||
"""Build the options menu entries specific to virtual power sensors."""
|
||||
menu: list[Step] = []
|
||||
if self.strategy and self.should_add_strategy_option_to_menu():
|
||||
menu.append(STRATEGY_STEP_MAPPING[self.strategy])
|
||||
if self.selected_profile:
|
||||
menu.append(Step.LIBRARY_OPTIONS)
|
||||
if self.should_add_select_device_to_menu():
|
||||
menu.append(Step.SELECT_DEVICE)
|
||||
menu.append(Step.ADVANCED_OPTIONS)
|
||||
return menu
|
||||
|
||||
def should_add_select_device_to_menu(self) -> bool:
|
||||
"""Check whether the device selection should be added to the menu."""
|
||||
if not self.selected_profile or self.selected_profile.discovery_by not in [
|
||||
DiscoveryBy.DEVICE,
|
||||
DiscoveryBy.CONFIG_ENTRY,
|
||||
]:
|
||||
return False
|
||||
|
||||
if self.selected_profile.discovery_by == DiscoveryBy.CONFIG_ENTRY:
|
||||
# Only devices of the source config entry are selectable, so there must be something to choose from.
|
||||
return len(self.flow_handlers[FlowType.LIBRARY].get_selectable_devices()) > 1
|
||||
|
||||
return True
|
||||
|
||||
def should_add_strategy_option_to_menu(self) -> bool:
|
||||
"""Check whether the strategy option should be added to the menu."""
|
||||
if not self.strategy or self.strategy not in STRATEGY_STEP_MAPPING:
|
||||
|
||||
@@ -240,7 +240,9 @@ DEFAULT_UTILITY_METER_TYPES = [DAILY, WEEKLY, MONTHLY]
|
||||
|
||||
DISCOVERY_SOURCE_ENTITY = "source_entity"
|
||||
DISCOVERY_POWER_PROFILES = "power_profiles"
|
||||
DISCOVERY_INTEGRATION_NAME = "integration_name"
|
||||
DISCOVERY_TYPE = "discovery_type"
|
||||
LIBRARY_DISCOVERY_IGNORED_DOMAINS = "discovery_ignored_domains"
|
||||
|
||||
LIBRARY_URL = "https://library.powercalc.nl"
|
||||
API_URL = "https://api.powercalc.nl"
|
||||
|
||||
@@ -31,6 +31,54 @@ def is_composite_device_id(hass: HomeAssistant, device_id: str) -> bool:
|
||||
return bool(is_composite(device_id))
|
||||
|
||||
|
||||
def get_related_device_ids(hass: HomeAssistant, device_id: str) -> set[str]:
|
||||
"""
|
||||
Return the IDs of all devices representing the same physical device, including `device_id` itself.
|
||||
Devices are related when they were split off from the same legacy composite device, or when they
|
||||
share any identifier or connection. Both happen when a single physical device is provided by more
|
||||
than one config entry.
|
||||
`device_id` may be the ID of a registered device, or of a legacy composite device.
|
||||
"""
|
||||
device_reg = device_registry.async_get(hass)
|
||||
related = {device_id}
|
||||
related.update(device.id for device in _get_composite_split_devices(device_reg, device_id))
|
||||
|
||||
device = device_reg.async_get(device_id)
|
||||
if device is None:
|
||||
return related
|
||||
|
||||
sibling_devices = _get_composite_split_devices(device_reg, getattr(device, "composite_device_id", None))
|
||||
related.update(sibling_device.id for sibling_device in sibling_devices)
|
||||
|
||||
# Only available in HA >=2026.8. On older versions devices sharing an identifier or connection
|
||||
# were merged into a single device, so there is nothing to relate.
|
||||
get_devices = getattr(device_reg, "async_get_devices", None)
|
||||
if callable(get_devices):
|
||||
related.update(
|
||||
linked_device.id
|
||||
for linked_device in get_devices(identifiers=device.identifiers, connections=device.connections)
|
||||
)
|
||||
|
||||
return related
|
||||
|
||||
|
||||
def _get_composite_split_devices(
|
||||
device_reg: device_registry.DeviceRegistry,
|
||||
composite_device_id: str | None,
|
||||
) -> list[DeviceEntry]:
|
||||
"""
|
||||
Return the devices a legacy composite device was split into.
|
||||
Returns an empty list when the ID does not identify a composite device, or when running on
|
||||
HA <2026.8, which does not split composite devices at all.
|
||||
"""
|
||||
if not composite_device_id:
|
||||
return []
|
||||
get_split_devices = getattr(device_reg, "async_get_devices_for_composite_device_id", None)
|
||||
if not callable(get_split_devices):
|
||||
return [] # pragma: no cover
|
||||
return list(get_split_devices(composite_device_id))
|
||||
|
||||
|
||||
def get_config_entry_ids(device: DeviceEntry) -> set[str]:
|
||||
"""
|
||||
Return the config entry IDs a device belongs to.
|
||||
@@ -39,7 +87,7 @@ def get_config_entry_ids(device: DeviceEntry) -> set[str]:
|
||||
"""
|
||||
if _HAS_SINGLE_CONFIG_ENTRY:
|
||||
return {device.config_entry_id}
|
||||
return set(getattr(device, "config_entries", set()))
|
||||
return set(getattr(device, "config_entries", set())) # pragma: no cover
|
||||
|
||||
|
||||
def get_first_device_for_config_entry(hass: HomeAssistant, config_entry_id: str) -> DeviceEntry | None:
|
||||
@@ -56,6 +104,18 @@ def get_devices_for_config_entry(hass: HomeAssistant, config_entry_id: str) -> l
|
||||
]
|
||||
|
||||
|
||||
def get_related_devices(hass: HomeAssistant, device_id: str) -> list[DeviceEntry]:
|
||||
"""Return all non-composite devices belonging to the same config entry as the given device."""
|
||||
device = device_registry.async_get(hass).async_get(device_id)
|
||||
if device is None:
|
||||
return []
|
||||
|
||||
devices: dict[str, DeviceEntry] = {}
|
||||
for config_entry_id in get_config_entry_ids(device):
|
||||
devices.update({related.id: related for related in get_devices_for_config_entry(hass, config_entry_id)})
|
||||
return list(devices.values())
|
||||
|
||||
|
||||
def attach_configured_device_entry(
|
||||
hass: HomeAssistant,
|
||||
sensor_config: ConfigType,
|
||||
|
||||
@@ -49,6 +49,6 @@ async def get_yaml_configuration(hass: HomeAssistant) -> ConfigType:
|
||||
try:
|
||||
yaml_config = await async_integration_yaml_config(hass, DOMAIN)
|
||||
return yaml_config.get(DOMAIN, {}) # type: ignore
|
||||
except Exception as err: # noqa: BLE001 # pragma: nocover
|
||||
_LOGGER.error("Could not retrieve YAML config: %s", err)
|
||||
except Exception: # pragma: nocover
|
||||
_LOGGER.exception("Could not retrieve YAML config")
|
||||
return {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timedelta
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
@@ -8,14 +8,15 @@ from typing import Any, TypeVar
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, SOURCE_USER, ConfigEntry
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM, CONF_UNIQUE_ID
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.const import CONF_DEVICE, CONF_ENTITY_ID, CONF_PLATFORM, CONF_UNIQUE_ID
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
from homeassistant.helpers import discovery_flow
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import IntegrationNotFound, async_get_integration
|
||||
|
||||
from .common import SourceEntity, create_source_entity
|
||||
from .const import (
|
||||
@@ -24,6 +25,7 @@ from .const import (
|
||||
CONF_MODEL,
|
||||
CONF_SENSORS,
|
||||
DATA_DISCOVERY_MANAGER,
|
||||
DISCOVERY_INTEGRATION_NAME,
|
||||
DISCOVERY_POWER_PROFILES,
|
||||
DISCOVERY_SOURCE_ENTITY,
|
||||
DOMAIN,
|
||||
@@ -31,7 +33,12 @@ from .const import (
|
||||
MANUFACTURER_WLED,
|
||||
CalculationStrategy,
|
||||
)
|
||||
from .device_binding import get_config_entry_ids, get_first_device_for_config_entry, is_composite_device_id
|
||||
from .device_binding import (
|
||||
get_config_entry_ids,
|
||||
get_first_device_for_config_entry,
|
||||
get_related_device_ids,
|
||||
is_composite_device_id,
|
||||
)
|
||||
from .group_include.filter import (
|
||||
CategoryFilter,
|
||||
CompositeFilter,
|
||||
@@ -180,6 +187,8 @@ class DiscoveryManager:
|
||||
continue # pragma: no cover
|
||||
|
||||
self.initialized_flows.add(entry.unique_id)
|
||||
self._initialize_configured_device(entry)
|
||||
|
||||
entity_id = entry.data.get(CONF_ENTITY_ID)
|
||||
if not entity_id or entity_id == DUMMY_ENTITY_ID:
|
||||
continue
|
||||
@@ -189,6 +198,23 @@ class DiscoveryManager:
|
||||
self.initialized_flows.add(f"pc_{entity.device_entry.id}")
|
||||
self.initialized_flows.add(entity_id)
|
||||
|
||||
def _initialize_configured_device(self, entry: ConfigEntry) -> None:
|
||||
"""Mark the device a config entry was setup for as already setup.
|
||||
|
||||
A single physical device can be represented by several device registry entries. HA >=2026.8
|
||||
splits devices belonging to multiple config entries into one device per entry, so the entry
|
||||
may hold the composite device ID, which no longer resolves to a registered device, or one of
|
||||
the split devices after the user resolved the composite device repair. Devices can also be
|
||||
registered by several integrations, in which case they share identifiers or connections.
|
||||
All of them are the device the user already configured, so none should be discovered again.
|
||||
"""
|
||||
device_id = entry.data.get(CONF_DEVICE)
|
||||
if not device_id:
|
||||
return
|
||||
|
||||
for related_device_id in get_related_device_ids(self.hass, str(device_id)):
|
||||
self.initialized_flows.add(f"pc_{related_device_id}")
|
||||
|
||||
def remove_initialized_flow(self, entry: ConfigEntry) -> None:
|
||||
"""Remove a flow from the initialized flows."""
|
||||
if entry.unique_id:
|
||||
@@ -199,17 +225,22 @@ class DiscoveryManager:
|
||||
|
||||
async def perform_discovery(
|
||||
self,
|
||||
source_provider: Callable[[], Awaitable[list[_DiscoverySourceT]]],
|
||||
source_creator: Callable[[_DiscoverySourceT], Awaitable[SourceEntity]],
|
||||
source_provider: Callable[[], list[_DiscoverySourceT]],
|
||||
source_creator: Callable[[_DiscoverySourceT], SourceEntity],
|
||||
discovery_type: DiscoveryBy,
|
||||
) -> None:
|
||||
"""Generalized discovery procedure for entities and devices."""
|
||||
for source in await source_provider():
|
||||
library = await self._get_library()
|
||||
ignored_domains = library.discovery_ignored_domains
|
||||
for source in source_provider():
|
||||
log_identifier = str(
|
||||
getattr(source, "entity_id", getattr(source, "id", getattr(source, "entry_id", "unknown"))),
|
||||
)
|
||||
try:
|
||||
source_entity = await source_creator(source)
|
||||
if self._is_domain_ignored(source, ignored_domains):
|
||||
_LOGGER.debug("%s: Integration domain is ignored, skipping discovery", log_identifier)
|
||||
continue
|
||||
source_entity = source_creator(source)
|
||||
model_info = await self.extract_model_info_from_device_info(
|
||||
source_entity.entity_entry or source_entity.device_entry,
|
||||
)
|
||||
@@ -235,13 +266,19 @@ class DiscoveryManager:
|
||||
)
|
||||
continue
|
||||
|
||||
self._init_entity_discovery(model_info, unique_id, source_entity, log_identifier, power_profiles, {})
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOGGER.error(
|
||||
"%s: Error during %s discovery: %s",
|
||||
await self._init_entity_discovery(
|
||||
model_info,
|
||||
unique_id,
|
||||
source_entity,
|
||||
log_identifier,
|
||||
power_profiles,
|
||||
{},
|
||||
)
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"%s: Error during %s discovery",
|
||||
log_identifier,
|
||||
discovery_type,
|
||||
err,
|
||||
)
|
||||
|
||||
async def discover_entity(
|
||||
@@ -256,12 +293,12 @@ class DiscoveryManager:
|
||||
|
||||
return await self.find_power_profiles(model_info, source_entity, discovery_type)
|
||||
|
||||
async def create_entity_source(self, entity_entry: er.RegistryEntry) -> SourceEntity:
|
||||
def create_entity_source(self, entity_entry: er.RegistryEntry) -> SourceEntity:
|
||||
"""Create SourceEntity for an entity."""
|
||||
return create_source_entity(entity_entry.entity_id, self.hass)
|
||||
|
||||
@staticmethod
|
||||
async def create_device_source(device_entry: dr.DeviceEntry) -> SourceEntity:
|
||||
def create_device_source(device_entry: dr.DeviceEntry) -> SourceEntity:
|
||||
"""Create SourceEntity for a device."""
|
||||
return SourceEntity(
|
||||
object_id=device_entry.name_by_user or device_entry.name or "",
|
||||
@@ -271,7 +308,7 @@ class DiscoveryManager:
|
||||
device_entry=device_entry,
|
||||
)
|
||||
|
||||
async def create_config_entry_source(self, config_entry: ConfigEntry) -> SourceEntity:
|
||||
def create_config_entry_source(self, config_entry: ConfigEntry) -> SourceEntity:
|
||||
"""Create a source representing all devices belonging to a config entry."""
|
||||
device_entry = get_first_device_for_config_entry(self.hass, config_entry.entry_id)
|
||||
return SourceEntity(
|
||||
@@ -341,6 +378,23 @@ class DiscoveryManager:
|
||||
|
||||
return power_profiles
|
||||
|
||||
def _is_domain_ignored(self, source: _DiscoverySourceT, ignored_domains: set[str]) -> bool:
|
||||
"""Return whether a discovery source belongs to a globally ignored integration domain."""
|
||||
if not ignored_domains:
|
||||
return False
|
||||
|
||||
if isinstance(source, er.RegistryEntry):
|
||||
return source.platform in ignored_domains
|
||||
if isinstance(source, ConfigEntry):
|
||||
return source.domain in ignored_domains
|
||||
|
||||
config_entry_id = next(iter(source.config_entries), None)
|
||||
if config_entry_id is None: # pragma: no cover
|
||||
return False
|
||||
|
||||
config_entry = self.hass.config_entries.async_get_entry(config_entry_id)
|
||||
return config_entry is not None and config_entry.domain in ignored_domains
|
||||
|
||||
async def init_wled_flow(self, model_info: ModelInfo, source_entity: SourceEntity) -> None:
|
||||
"""Initialize the discovery flow for a WLED light."""
|
||||
if DeviceType.LIGHT in self._exclude_device_types:
|
||||
@@ -358,7 +412,7 @@ class DiscoveryManager:
|
||||
)
|
||||
return
|
||||
|
||||
self._init_entity_discovery(
|
||||
await self._init_entity_discovery(
|
||||
model_info,
|
||||
unique_id,
|
||||
source_entity,
|
||||
@@ -379,7 +433,7 @@ class DiscoveryManager:
|
||||
and not re.search("master|segment", str(entity_entry.entity_id), flags=re.IGNORECASE)
|
||||
)
|
||||
|
||||
async def get_entities(self) -> list[er.RegistryEntry]:
|
||||
def get_entities(self) -> list[er.RegistryEntry]:
|
||||
"""Get all entities from entity registry which qualifies for discovery."""
|
||||
|
||||
def _check_already_configured(entity: er.RegistryEntry) -> bool:
|
||||
@@ -409,7 +463,7 @@ class DiscoveryManager:
|
||||
)
|
||||
return get_filtered_entity_list(self.hass, NotFilter(entity_filter))
|
||||
|
||||
async def get_devices(self) -> list[dr.DeviceEntry]:
|
||||
def get_devices(self) -> list[dr.DeviceEntry]:
|
||||
"""Fetch device entries."""
|
||||
return [
|
||||
device
|
||||
@@ -417,7 +471,7 @@ class DiscoveryManager:
|
||||
if not is_composite_device_id(self.hass, device.id)
|
||||
]
|
||||
|
||||
async def get_config_entries(self) -> list[ConfigEntry]:
|
||||
def get_config_entries(self) -> list[ConfigEntry]:
|
||||
"""Fetch config entries which have at least one non-composite device."""
|
||||
config_entry_ids = {
|
||||
config_entry_id
|
||||
@@ -518,8 +572,7 @@ class DiscoveryManager:
|
||||
|
||||
return await self.get_model_information_from_device(device_entry)
|
||||
|
||||
@callback
|
||||
def _init_entity_discovery(
|
||||
async def _init_entity_discovery(
|
||||
self,
|
||||
model_info: ModelInfo,
|
||||
unique_id: str,
|
||||
@@ -532,6 +585,7 @@ class DiscoveryManager:
|
||||
|
||||
discovery_data: dict[str, Any] = {
|
||||
CONF_ENTITY_ID: source_entity.entity_id,
|
||||
DISCOVERY_INTEGRATION_NAME: await self._get_integration_name(source_entity),
|
||||
DISCOVERY_SOURCE_ENTITY: source_entity,
|
||||
CONF_UNIQUE_ID: unique_id,
|
||||
}
|
||||
@@ -564,6 +618,27 @@ class DiscoveryManager:
|
||||
data=discovery_data,
|
||||
)
|
||||
|
||||
async def _get_integration_name(self, source_entity: SourceEntity) -> str | None:
|
||||
"""Return the display name of the integration which owns the discovery source."""
|
||||
config_entry_id = source_entity.config_entry_id
|
||||
if config_entry_id is None and source_entity.entity_entry:
|
||||
config_entry_id = source_entity.entity_entry.config_entry_id
|
||||
if config_entry_id is None and source_entity.device_entry:
|
||||
config_entry_id = next(iter(get_config_entry_ids(source_entity.device_entry)), None)
|
||||
if config_entry_id is None:
|
||||
return None
|
||||
|
||||
config_entry = self.hass.config_entries.async_get_entry(config_entry_id)
|
||||
if config_entry is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
integration = await async_get_integration(self.hass, config_entry.domain)
|
||||
except IntegrationNotFound:
|
||||
_LOGGER.debug("Unable to resolve integration name for domain %s", config_entry.domain)
|
||||
return None
|
||||
return integration.name
|
||||
|
||||
@property
|
||||
def status(self) -> DiscoveryStatus:
|
||||
"""Get the discovery status"""
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.const import CONF_DEVICE
|
||||
from homeassistant.helpers import selector, translation
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.powercalc.const import (
|
||||
@@ -20,7 +21,7 @@ from custom_components.powercalc.const import (
|
||||
LIBRARY_URL,
|
||||
CalculationStrategy,
|
||||
)
|
||||
from custom_components.powercalc.device_binding import get_devices_for_config_entry
|
||||
from custom_components.powercalc.device_binding import get_devices_for_config_entry, get_related_devices
|
||||
from custom_components.powercalc.discovery import (
|
||||
get_power_profile_by_source_device,
|
||||
get_power_profile_by_source_entity,
|
||||
@@ -225,33 +226,51 @@ class LibraryFlow:
|
||||
|
||||
async def async_step_select_device(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||
"""Ask which device should receive the entities created for a config-entry profile."""
|
||||
assert self.flow.source_entity is not None
|
||||
assert self.flow.source_entity.config_entry_id is not None
|
||||
devices = get_devices_for_config_entry(self.flow.hass, self.flow.source_entity.config_entry_id)
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.SELECT_DEVICE,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DEVICE): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[
|
||||
selector.SelectOptionDict(
|
||||
value=device.id,
|
||||
label=device.name_by_user or device.name or device.id,
|
||||
)
|
||||
for device in devices
|
||||
],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
schema=self.build_select_device_schema(),
|
||||
next_step=Step.POST_LIBRARY,
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
def build_select_device_schema(self) -> vol.Schema:
|
||||
"""Build the schema to select the device the Powercalc entities should be linked to."""
|
||||
discovery_by = self.flow.selected_profile.discovery_by if self.flow.selected_profile else DiscoveryBy.ENTITY
|
||||
|
||||
if discovery_by == DiscoveryBy.CONFIG_ENTRY:
|
||||
# The entities can only be linked to one of the devices of the source config entry.
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DEVICE): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[
|
||||
selector.SelectOptionDict(
|
||||
value=device.id,
|
||||
label=device.name_by_user or device.name or device.id,
|
||||
)
|
||||
for device in self.get_selectable_devices()
|
||||
],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
return vol.Schema({vol.Required(CONF_DEVICE): selector.DeviceSelector()})
|
||||
|
||||
def get_selectable_devices(self) -> list[DeviceEntry]:
|
||||
"""Return the devices belonging to the same config entry as the source of the profile."""
|
||||
config_entry_id = self.flow.source_entity.config_entry_id if self.flow.source_entity else None
|
||||
if config_entry_id:
|
||||
return get_devices_for_config_entry(self.flow.hass, config_entry_id)
|
||||
|
||||
# The options flow has no reference to the source config entry anymore,
|
||||
# so resolve the candidates from the currently configured device instead.
|
||||
device_id: str | None = self.flow.sensor_config.get(CONF_DEVICE)
|
||||
return get_related_devices(self.flow.hass, device_id) if device_id else []
|
||||
|
||||
async def _async_next_strategy_step(self, profile: PowerProfile) -> ConfigFlowResult | None:
|
||||
"""Return the next step needed to configure the calculation strategy, or None when nothing is left to ask."""
|
||||
handled_steps = self.flow.handled_steps
|
||||
@@ -641,6 +660,14 @@ class LibraryOptionsFlow(LibraryFlow):
|
||||
super().__init__(flow)
|
||||
self.flow: PowercalcOptionsFlow = flow
|
||||
|
||||
async def async_step_select_device(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||
"""Change the device the Powercalc entities are linked to."""
|
||||
return await self.flow.async_handle_options_step(
|
||||
user_input,
|
||||
self.build_select_device_schema(),
|
||||
Step.SELECT_DEVICE,
|
||||
)
|
||||
|
||||
async def async_step_library_options(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||
"""Handle the basic options flow."""
|
||||
self.flow.is_library_flow = True
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"requirements": [
|
||||
"numpy>=1.21.1"
|
||||
],
|
||||
"version": "v1.24.0"
|
||||
"version": "v1.24.1"
|
||||
}
|
||||
@@ -52,6 +52,11 @@ class ProfileLibrary:
|
||||
async def initialize(self) -> None:
|
||||
await self._loader.initialize()
|
||||
|
||||
@property
|
||||
def discovery_ignored_domains(self) -> set[str]:
|
||||
"""Get integration domains globally excluded from discovery."""
|
||||
return self._loader.get_discovery_ignored_domains()
|
||||
|
||||
@staticmethod
|
||||
@singleton("powercalc_library")
|
||||
async def factory(hass: HomeAssistant) -> ProfileLibrary:
|
||||
|
||||
@@ -15,6 +15,10 @@ class CompositeLoader(Loader):
|
||||
for loader in self.loaders:
|
||||
await loader.initialize()
|
||||
|
||||
def get_discovery_ignored_domains(self) -> set[str]:
|
||||
"""Get all integration domains excluded by the combined libraries."""
|
||||
return {domain for loader in self.loaders for domain in loader.get_discovery_ignored_domains()}
|
||||
|
||||
async def get_manufacturer_listing(
|
||||
self,
|
||||
device_types: set[DeviceType] | None,
|
||||
|
||||
@@ -25,6 +25,10 @@ class LocalLoader(Loader):
|
||||
if not self._is_custom_directory:
|
||||
await self._hass.async_add_executor_job(self._load_custom_library)
|
||||
|
||||
def get_discovery_ignored_domains(self) -> set[str]:
|
||||
"""Local profile directories do not provide global library metadata."""
|
||||
return set()
|
||||
|
||||
async def get_manufacturer_listing(
|
||||
self,
|
||||
device_types: set[DeviceType] | None,
|
||||
|
||||
@@ -7,6 +7,9 @@ class Loader(Protocol):
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize the loader."""
|
||||
|
||||
def get_discovery_ignored_domains(self) -> set[str]:
|
||||
"""Get integration domains excluded from discovery."""
|
||||
|
||||
async def get_manufacturer_listing(
|
||||
self,
|
||||
device_types: set[DeviceType] | None,
|
||||
|
||||
@@ -16,7 +16,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.storage import STORAGE_DIR
|
||||
from homeassistant.loader import async_get_integration
|
||||
|
||||
from custom_components.powercalc.const import API_URL, BUILT_IN_LIBRARY_DIR, DOMAIN
|
||||
from custom_components.powercalc.const import (
|
||||
API_URL,
|
||||
BUILT_IN_LIBRARY_DIR,
|
||||
DOMAIN,
|
||||
LIBRARY_DISCOVERY_IGNORED_DOMAINS,
|
||||
)
|
||||
from custom_components.powercalc.helpers import async_cache, clear_async_cache
|
||||
from custom_components.powercalc.power_profile.error import LibraryLoadingError, ProfileDownloadError
|
||||
from custom_components.powercalc.power_profile.loader.protocol import Loader
|
||||
@@ -80,6 +85,10 @@ class RemoteLoader(Loader):
|
||||
for manufacturer in manufacturers:
|
||||
self._index_manufacturer(manufacturer, powercalc_version)
|
||||
|
||||
def get_discovery_ignored_domains(self) -> set[str]:
|
||||
"""Get integration domains excluded from discovery by library metadata."""
|
||||
return set(self.library_contents.get(LIBRARY_DISCOVERY_IGNORED_DOMAINS, []))
|
||||
|
||||
def _index_manufacturer(self, manufacturer: LibraryManufacturer, powercalc_version: AwesomeVersion) -> None:
|
||||
"""Register a manufacturer, its aliases and all of its supported models in the lookup tables."""
|
||||
manufacturer_name = str(manufacturer.get("dir_name"))
|
||||
|
||||
@@ -538,11 +538,8 @@ async def handle_nested_entity(
|
||||
),
|
||||
)
|
||||
entities_to_add.extend_items(child_entities)
|
||||
except SensorConfigurationError as exception:
|
||||
_LOGGER.error(
|
||||
"Group state might be misbehaving because there was an error with an entity",
|
||||
exc_info=exception,
|
||||
)
|
||||
except SensorConfigurationError:
|
||||
_LOGGER.exception("Group state might be misbehaving because there was an error with an entity")
|
||||
|
||||
|
||||
async def add_discovered_entities(
|
||||
|
||||
@@ -938,8 +938,8 @@ class PreviousStateStore:
|
||||
instance.states[group] = {
|
||||
entity_id: State.from_dict(json_state) for (entity_id, json_state) in entities.items()
|
||||
}
|
||||
except HomeAssistantError as exc: # pragma: no cover
|
||||
_LOGGER.error("Error loading previous energy sensor states", exc_info=exc)
|
||||
except HomeAssistantError: # pragma: no cover
|
||||
_LOGGER.exception("Error loading previous energy sensor states")
|
||||
|
||||
instance.async_setup_dump()
|
||||
|
||||
@@ -983,8 +983,8 @@ class PreviousStateStore:
|
||||
"""Save the current states to storage."""
|
||||
try:
|
||||
await self.store.async_save(self.states)
|
||||
except HomeAssistantError as exc: # pragma: no cover
|
||||
_LOGGER.error("Error saving current states", exc_info=exc)
|
||||
except HomeAssistantError: # pragma: no cover
|
||||
_LOGGER.exception("Error saving current states")
|
||||
|
||||
@callback
|
||||
def async_setup_dump(self) -> None:
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Možnosti playbooku",
|
||||
"multi_switch": "Možnosti vícenásobného spínače",
|
||||
"real_power": "Možnosti skutečného výkonu",
|
||||
"select_device": "Zařízení",
|
||||
"utility_meter_options": "Možnosti měřičů spotřeby",
|
||||
"wled": "Možnosti WLED",
|
||||
"cost_options": "Možnosti nákladů"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Možnosti knihovny",
|
||||
"description": "Aktuálně je vybrán následující profil knihovny: \n výrobce: {manufacturer}\n model: {model}\n\nPokud chcete profil změnit, klikněte na Další."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Zařízení"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Zařízení, ke kterému mají být entity Powercalc připojeny"
|
||||
},
|
||||
"description": "Vyberte zařízení, ke kterému má být senzor Powercalc připojen.",
|
||||
"title": "Vyberte zařízení"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Lineární možnosti",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Playbook-indstillinger",
|
||||
"multi_switch": "Indstillinger for multikontakt",
|
||||
"real_power": "Indstillinger for fysisk effekt",
|
||||
"select_device": "Enhed",
|
||||
"utility_meter_options": "Indstillinger for forbrugsmålere",
|
||||
"wled": "WLED-indstillinger",
|
||||
"cost_options": "Omkostningsindstillinger"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Biblioteksindstillinger",
|
||||
"description": "I øjeblikket er følgende biblioteksprofil valgt: \n producent: {manufacturer}\n model: {model}\n\nHvis du vil ændre profilen, skal du klikke på næste."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Enhed"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Den enhed, som Powercalc-entiteterne skal knyttes til"
|
||||
},
|
||||
"description": "Vælg den enhed, som Powercalc-sensoren skal knyttes til.",
|
||||
"title": "Vælg enhed"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Lineære indstillinger",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Playbook-Optionen",
|
||||
"multi_switch": "Mehrfachschalter-Optionen",
|
||||
"real_power": "Optionen für reale Leistung",
|
||||
"select_device": "Gerät",
|
||||
"utility_meter_options": "Verbrauchszähler-Optionen",
|
||||
"wled": "WLED Einstellung",
|
||||
"cost_options": "Kostenoptionen"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Bibliotheksoptionen",
|
||||
"description": "Derzeit ist folgendes Bibliotheksprofil ausgewählt: \n Hersteller: {manufacturer}\n Modell: {model}\n\nWenn du das Profil ändern möchtest, klicke auf Weiter."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Gerät"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Das Gerät, mit dem die Powercalc-Entitäten verknüpft werden sollen"
|
||||
},
|
||||
"description": "Wählen Sie das Gerät aus, mit dem der Powercalc-Sensor verknüpft werden soll.",
|
||||
"title": "Gerät auswählen"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Lineare Optionen",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Playbook options",
|
||||
"multi_switch": "Multi switch options",
|
||||
"real_power": "Real power options",
|
||||
"select_device": "Device",
|
||||
"utility_meter_options": "Utility meter options",
|
||||
"wled": "WLED options",
|
||||
"cost_options": "Cost options"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Library options",
|
||||
"description": "Currently the following library profile is selected: \n manufacturer: {manufacturer}\n model: {model}\n\nIf you want to change the profile, click next."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Device"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "The device the Powercalc entities should be linked to"
|
||||
},
|
||||
"description": "Select the device to which the Powercalc sensor should be linked.",
|
||||
"title": "Select device"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Linear options",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Opciones de Reproducciones",
|
||||
"multi_switch": "Opciones multi-interruptor",
|
||||
"real_power": "Opciones de potencia real",
|
||||
"select_device": "Dispositivo",
|
||||
"utility_meter_options": "Opciones de contador eléctrico",
|
||||
"wled": "Opciones de WLED",
|
||||
"cost_options": "Opciones de costos"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Opciones de biblioteca",
|
||||
"description": "Actualmente se ha seleccionado el siguiente perfil de biblioteca: \n fabricante: {manufacturer}\n modelo: {model}\n\nSi quieres cambiar el perfil, haz clic a continuación."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Dispositivo"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "El dispositivo al que se deben vincular las entidades de Powercalc"
|
||||
},
|
||||
"description": "Seleccione el dispositivo al que se debe vincular el sensor de Powercalc.",
|
||||
"title": "Seleccionar dispositivo"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Opciones lineales",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Playbookin vaihtoehdot",
|
||||
"multi_switch": "Useita kytkinvaihtoehtoja",
|
||||
"real_power": "Todelliset tehovaihtoehdot",
|
||||
"select_device": "Laite",
|
||||
"utility_meter_options": "Kulutusmittarin asetukset",
|
||||
"wled": "WLED vaihtoehdot",
|
||||
"cost_options": "Kustannusvaihtoehdot"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Kirjaston vaihtoehdot",
|
||||
"description": "Tällä hetkellä on valittuna seuraava kirjastoprofiili: \n valmistaja: {manufacturer}\n malli: {model}\n\nJos haluat vaihtaa profiilia, napsauta Seuraava."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Laite"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Laite, johon Powercalc-entiteetit liitetään"
|
||||
},
|
||||
"description": "Valitse laite, johon Powercalc-anturi liitetään.",
|
||||
"title": "Valitse laite"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Lineaariset vaihtoehdot",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Options du playbook",
|
||||
"multi_switch": "Options du multi-interrupteur",
|
||||
"real_power": "Options de puissance réelle",
|
||||
"select_device": "Appareil",
|
||||
"utility_meter_options": "Options des compteurs de services publics",
|
||||
"wled": "Options WLED",
|
||||
"cost_options": "Options de coût"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Options de bibliothèque",
|
||||
"description": "Le profil de bibliothèque suivant est actuellement sélectionné : \n fabricant : {manufacturer}\n modèle : {model}\n\nSi vous souhaitez changer de profil, cliquez sur Suivant."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Appareil"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "L’appareil auquel les entités Powercalc doivent être associées"
|
||||
},
|
||||
"description": "Sélectionnez l’appareil auquel le capteur Powercalc doit être associé.",
|
||||
"title": "Sélectionner un appareil"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Options linéaires",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Playbook beállításai",
|
||||
"multi_switch": "Többkapcsolós beállítások",
|
||||
"real_power": "Valós teljesítmény beállításai",
|
||||
"select_device": "Eszköz",
|
||||
"utility_meter_options": "Közüzemi mérő beállításai",
|
||||
"wled": "WLED beállításai",
|
||||
"cost_options": "Költségbeállítások"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Könyvtári beállítások",
|
||||
"description": "Jelenleg a következő könyvtári profil van kiválasztva: \n gyártó: {manufacturer}\n modell: {model}\n\nHa módosítani szeretné a profilt, kattintson a Tovább gombra."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Eszköz"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Az eszköz, amelyhez a Powercalc-entitásokat hozzá kell rendelni"
|
||||
},
|
||||
"description": "Válassza ki azt az eszközt, amelyhez a Powercalc-érzékelőt hozzá kell rendelni.",
|
||||
"title": "Eszköz kiválasztása"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Lineáris beállítások",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Opzioni del playbook",
|
||||
"multi_switch": "Opzioni multiinterruttore",
|
||||
"real_power": "Opzioni di potenza reale",
|
||||
"select_device": "Dispositivo",
|
||||
"utility_meter_options": "Opzioni del contatore di utilità",
|
||||
"wled": "Opzioni WLED",
|
||||
"cost_options": "Opzioni di costo"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Opzioni della libreria",
|
||||
"description": "Attualmente è selezionato il seguente profilo libreria: \n produttore: {manufacturer}\n modello: {model}\n\nSe desideri modificare il profilo, fai clic su Avanti."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Dispositivo"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Il dispositivo a cui devono essere collegate le entità Powercalc"
|
||||
},
|
||||
"description": "Seleziona il dispositivo a cui deve essere collegato il sensore Powercalc.",
|
||||
"title": "Seleziona dispositivo"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Opzioni lineari",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Playbook-alternativer",
|
||||
"multi_switch": "Alternativer for flere brytere",
|
||||
"real_power": "Innstillinger for målt effekt",
|
||||
"select_device": "Enhet",
|
||||
"utility_meter_options": "Innstillinger for forbruksmåler",
|
||||
"wled": "WLED-innstillinger",
|
||||
"cost_options": "Kostnadsalternativer"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Alternativer for bibliotek",
|
||||
"description": "For øyeblikket er følgende bibliotekprofil valgt: \n produsent: {manufacturer}\n modell: {model}\n\nHvis du vil endre profilen, klikker du på neste."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Enhet"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Enheten Powercalc-entitetene skal knyttes til"
|
||||
},
|
||||
"description": "Velg enheten som Powercalc-sensoren skal knyttes til.",
|
||||
"title": "Velg enhet"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Lineære alternativer",
|
||||
"data": {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"already_configured": "Sensor is reeds geconfigureerd, specifieer een uniek ID"
|
||||
},
|
||||
"error": {
|
||||
"cost_price_mandatory": "U moet een energieprijs of een energieprijssensor aanleveren",
|
||||
"cost_price_mandatory": "U moet een energieprijs of een sensor van de energieprijs leveren",
|
||||
"daily_energy_mandatory": "Je moet minimaal waarde of waarde template opgeven",
|
||||
"entity_mandatory": "Je moet verplicht een entiteit opgeven voor iedere andere strategie dan playbook",
|
||||
"fixed_mandatory": "Je dient minimaal een van de volgende velden te definiëren: Vermogen, Vermogen template of Vermogen per status",
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Playbook opties",
|
||||
"multi_switch": "Multi switch opties",
|
||||
"real_power": "Opties voor werkelijk vermogen",
|
||||
"select_device": "Apparaat",
|
||||
"utility_meter_options": "Nutsmeter opties",
|
||||
"wled": "WLED opties",
|
||||
"cost_options": "Kosten opties"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Bibliotheek opties",
|
||||
"description": "Momenteel is het huidige profiel geselecteerd: \nfabrikant: {manufacturer}\nmodel: {model}\n\nAls u het profiel wilt wijzigen, klikt u op volgende."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Apparaat"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Het apparaat waaraan de Powercalc-entiteiten moeten worden gekoppeld"
|
||||
},
|
||||
"description": "Selecteer het apparaat waaraan de Powercalc-sensor moet worden gekoppeld.",
|
||||
"title": "Apparaat selecteren"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Lineaire opties",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Opcje playbook",
|
||||
"multi_switch": "Opcje wieloprzełącznika",
|
||||
"real_power": "Opcje rzeczywistej mocy",
|
||||
"select_device": "Urządzenie",
|
||||
"utility_meter_options": "Opcje licznika mediów",
|
||||
"wled": "Opcje WLED",
|
||||
"cost_options": "Opcje kosztów"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Opcje biblioteki",
|
||||
"description": "Obecnie wybrany jest następujący profil biblioteki: \n producent: {manufacturer}\n model: {model}\n\nJeśli chcesz zmienić profil, kliknij dalej."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Urządzenie"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Urządzenie, z którym mają zostać powiązane encje Powercalc"
|
||||
},
|
||||
"description": "Wybierz urządzenie, z którym ma zostać powiązany czujnik Powercalc.",
|
||||
"title": "Wybierz urządzenie"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Opcje liniowe",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Opções de roteiro",
|
||||
"multi_switch": "Opções de multi-interruptor",
|
||||
"real_power": "Opções de potência real",
|
||||
"select_device": "Dispositivo",
|
||||
"utility_meter_options": "Opções do medidor de utilidade",
|
||||
"wled": "Opções de WLED",
|
||||
"cost_options": "Opções de custo"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Opções da biblioteca",
|
||||
"description": "Atualmente o seguinte perfil de biblioteca esta selecionado: \n fabricante {manufacturer}\n modelo: {model}\n\nSe você deseja alterar o perfil, clique em próximo."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Dispositivo"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "O dispositivo ao qual as entidades do Powercalc devem ser vinculadas"
|
||||
},
|
||||
"description": "Selecione o dispositivo ao qual o sensor do Powercalc deve ser vinculado.",
|
||||
"title": "Selecionar dispositivo"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Opções linear",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Opções de sequência",
|
||||
"multi_switch": "Opções de interruptor múltiplo",
|
||||
"real_power": "Opções de potência real",
|
||||
"select_device": "Dispositivo",
|
||||
"utility_meter_options": "Opções de contador de serviços",
|
||||
"wled": "Opções WLED",
|
||||
"cost_options": "Opções de custo"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Opções da biblioteca",
|
||||
"description": "Atualmente está selecionado o seguinte perfil da biblioteca: \n fabricante: {manufacturer}\n modelo: {model}\n\nSe quiser alterar o perfil, clique em seguinte."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Dispositivo"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "O dispositivo ao qual as entidades do Powercalc devem ser associadas"
|
||||
},
|
||||
"description": "Selecione o dispositivo ao qual o sensor do Powercalc deve ser associado.",
|
||||
"title": "Selecionar dispositivo"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Opções lineares",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Opțiuni de playbook",
|
||||
"multi_switch": "Opțiuni de comutare multiplă",
|
||||
"real_power": "Opțiuni de putere reală",
|
||||
"select_device": "Dispozitiv",
|
||||
"utility_meter_options": "Opțiuni de contor de utilitate",
|
||||
"wled": "Opțiuni WLED",
|
||||
"cost_options": "Opțiuni de cost"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Opțiuni de bibliotecă",
|
||||
"description": "În prezent este selectat următorul profil de bibliotecă: \n producator: {manufacturer}\n model: {model}\n\nDacă doriți să schimbați profilul, faceți clic pe următorul."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Dispozitiv"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Dispozitivul la care trebuie asociate entitățile Powercalc"
|
||||
},
|
||||
"description": "Selectați dispozitivul la care trebuie asociat senzorul Powercalc.",
|
||||
"title": "Selectați dispozitivul"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Opțiuni liniare",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Параметры сценариев",
|
||||
"multi_switch": "Параметры мультивыключателя",
|
||||
"real_power": "Параметры реальной мощности",
|
||||
"select_device": "Устройство",
|
||||
"utility_meter_options": "Параметры счётчиков",
|
||||
"wled": "Параметры WLED",
|
||||
"cost_options": "Варианты стоимости"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Параметры библиотеки",
|
||||
"description": "В настоящее время выбран следующий профиль библиотеки: \n производитель: {manufacturer}\n модель: {model}\n\nЕсли хотите изменить профиль, нажмите далее."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Устройство"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Устройство, к которому следует привязать сущности Powercalc"
|
||||
},
|
||||
"description": "Выберите устройство, к которому следует привязать датчик Powercalc.",
|
||||
"title": "Выберите устройство"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Линейные параметры",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Možnosti playbooku",
|
||||
"multi_switch": "Možnosti viacerých prepínačov",
|
||||
"real_power": "Skutočné možnosti napájania",
|
||||
"select_device": "Zariadenie",
|
||||
"utility_meter_options": "Možnosti elektromera",
|
||||
"wled": "WLED možnosti",
|
||||
"cost_options": "Možnosti nákladov"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Možnosti knižnice",
|
||||
"description": "Momentálne je vybratý nasledujúci profil knižnice: \n výrobca: {manufacturer}\n model: {model}\n\nAk chcete profil zmeniť, kliknite na tlačidlo Ďalej."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Zariadenie"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Zariadenie, ku ktorému majú byť priradené entity Powercalc"
|
||||
},
|
||||
"description": "Vyberte zariadenie, ku ktorému má byť priradený senzor Powercalc.",
|
||||
"title": "Vyberte zariadenie"
|
||||
},
|
||||
"linear": {
|
||||
"title": "lineárne možnosti",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "Playbook-alternativ",
|
||||
"multi_switch": "Alternativ för flera switchar",
|
||||
"real_power": "Verkliga kraftalternativ",
|
||||
"select_device": "Enhet",
|
||||
"utility_meter_options": "Alternativ för verktygsmätare",
|
||||
"wled": "WLED alternativ",
|
||||
"cost_options": "Kostnadsalternativ"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "Biblioteksalternativ",
|
||||
"description": "För närvarande är följande biblioteksprofil vald: \n tillverkare: {manufacturer}\n modell: {model}\n\nOm du vill ändra profilen klickar du på nästa."
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "Enhet"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Enheten som Powercalc-entiteterna ska länkas till"
|
||||
},
|
||||
"description": "Välj enheten som Powercalc-sensorn ska länkas till.",
|
||||
"title": "Välj enhet"
|
||||
},
|
||||
"linear": {
|
||||
"title": "Linjära alternativ",
|
||||
"data": {
|
||||
|
||||
@@ -971,6 +971,7 @@
|
||||
"playbook": "运行方案选项",
|
||||
"multi_switch": "多路开关选项",
|
||||
"real_power": "实际功率选项",
|
||||
"select_device": "设备",
|
||||
"utility_meter_options": "公用事业计量表选项",
|
||||
"wled": "WLED 选项",
|
||||
"cost_options": "费用选项"
|
||||
@@ -980,6 +981,16 @@
|
||||
"title": "配置文件库选项",
|
||||
"description": "当前选择了以下配置文件库中的配置文件:\n 制造商:{manufacturer}\n 型号:{model}\n\n如需更改配置文件,请点击“下一步”。"
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"device": "设备"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Powercalc 实体应关联到的设备"
|
||||
},
|
||||
"description": "选择 Powercalc 传感器应关联到的设备。",
|
||||
"title": "选择设备"
|
||||
},
|
||||
"linear": {
|
||||
"title": "线性选项",
|
||||
"data": {
|
||||
|
||||
@@ -34,6 +34,7 @@ from .token_manager import TokenManager
|
||||
PLATFORMS = [
|
||||
"light",
|
||||
"switch",
|
||||
"fan",
|
||||
"lock",
|
||||
"climate",
|
||||
"alarm_control_panel",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,6 +15,7 @@ LOCK_UPDATED = f"{DOMAIN}.lock_updated"
|
||||
CAMERA_UPDATED = f"{DOMAIN}.camera_updated"
|
||||
LIGHT_UPDATED = f"{DOMAIN}.light_updated"
|
||||
COVER_UPDATED = f"{DOMAIN}.cover_updated"
|
||||
AIR_PURIFIER_UPDATED = f"{DOMAIN}.air_purifier_updated"
|
||||
RESET_BUTTON_PRESSED = f"{DOMAIN}.reset_button_pressed"
|
||||
# EVENT NAMES
|
||||
WYZE_CAMERA_EVENT = "wyze_camera_event"
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
"issue_tracker": "https://github.com/SecKatie/ha-wyzeapi/issues",
|
||||
"loggers": ["custom_components.wyzeapi"],
|
||||
"requirements": [
|
||||
"wyzeapy>=0.5.33,<0.6",
|
||||
"wyzeapy>=0.6.1,<0.7",
|
||||
"websockets"
|
||||
],
|
||||
"version": "0.1.38"
|
||||
"version": "0.1.39"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from wyzeapy import Wyzeapy
|
||||
from wyzeapy.services.air_purifier_service import AirPurifier
|
||||
from wyzeapy.services.camera_service import Camera
|
||||
from wyzeapy.services.irrigation_service import Irrigation, IrrigationService
|
||||
from wyzeapy.services.lock_service import Lock
|
||||
@@ -36,6 +37,7 @@ from homeassistant.helpers.event import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
AIR_PURIFIER_UPDATED,
|
||||
CAMERA_UPDATED,
|
||||
CONF_CLIENT,
|
||||
DOMAIN,
|
||||
@@ -71,6 +73,7 @@ async def async_setup_entry(
|
||||
camera_service = await client.camera_service
|
||||
switch_usage_service = await client.switch_usage_service
|
||||
irrigation_service = await client.irrigation_service
|
||||
air_purifier_service = await client.air_purifier_service
|
||||
|
||||
locks = await lock_service.get_locks()
|
||||
sensors = []
|
||||
@@ -95,6 +98,11 @@ async def async_setup_entry(
|
||||
sensors.append(WyzePlugEnergySensor(plug, switch_usage_service))
|
||||
sensors.append(WyzePlugDailyEnergySensor(plug))
|
||||
|
||||
air_purifiers = await air_purifier_service.get_air_purifiers()
|
||||
for air_purifier in air_purifiers:
|
||||
sensors.append(WyzeAirPurifierAQISensor(air_purifier))
|
||||
sensors.append(WyzeAirPurifierHourlyMaxAQISensor(air_purifier))
|
||||
|
||||
# Get all irrigation devices
|
||||
irrigation_devices = await irrigation_service.get_irrigations()
|
||||
|
||||
@@ -624,3 +632,140 @@ class WyzeIrrigationSSID(WyzeIrrigationBaseSensor):
|
||||
def native_value(self) -> str:
|
||||
"""Return the SSID."""
|
||||
return self._device.ssid
|
||||
|
||||
|
||||
class WyzeAirPurifierAirQualitySensor(SensorEntity):
|
||||
"""Base class for Wyze Air Purifier air quality sensors."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_device_class = SensorDeviceClass.AQI
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_suggested_display_precision = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
air_purifier: AirPurifier,
|
||||
) -> None:
|
||||
"""Initialize the AQI sensor."""
|
||||
self._air_purifier = air_purifier
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this entity."""
|
||||
device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._air_purifier.mac)},
|
||||
name=self._air_purifier.nickname,
|
||||
manufacturer="WyzeLabs",
|
||||
model=self._air_purifier.product_model,
|
||||
)
|
||||
if self._air_purifier.app_version:
|
||||
device_info["sw_version"] = self._air_purifier.app_version
|
||||
if self._air_purifier.sn:
|
||||
device_info["serial_number"] = self._air_purifier.sn
|
||||
if self._air_purifier.wifi_mac:
|
||||
device_info["connections"] = {
|
||||
(dr.CONNECTION_NETWORK_MAC, self._air_purifier.wifi_mac)
|
||||
}
|
||||
return device_info
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return the connection status of this sensor."""
|
||||
return self._air_purifier.available
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return device attributes of the entity."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
"device model": self._air_purifier.product_model,
|
||||
}
|
||||
|
||||
@callback
|
||||
def handle_air_purifier_update(self, air_purifier: AirPurifier) -> None:
|
||||
"""Handle air purifier updates."""
|
||||
self._air_purifier = air_purifier
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Add listener on startup."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{AIR_PURIFIER_UPDATED}-{self._air_purifier.mac}",
|
||||
self.handle_air_purifier_update,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class WyzeAirPurifierAQISensor(WyzeAirPurifierAirQualitySensor):
|
||||
"""Representation of a Wyze Air Purifier current AQI sensor."""
|
||||
|
||||
_attr_name = "Current AQI"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
air_purifier: AirPurifier,
|
||||
) -> None:
|
||||
"""Initialize the current AQI sensor."""
|
||||
super().__init__(air_purifier)
|
||||
self._attr_unique_id = f"{self._air_purifier.mac}-aqi"
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
"""Return the current AQI value."""
|
||||
return self._air_purifier.aqi
|
||||
|
||||
|
||||
class WyzeAirPurifierHourlyMaxAQISensor(WyzeAirPurifierAirQualitySensor):
|
||||
"""Representation of a Wyze Air Purifier hourly max AQI sensor."""
|
||||
|
||||
_attr_name = "Hourly Max AQI"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
air_purifier: AirPurifier,
|
||||
) -> None:
|
||||
"""Initialize the hourly max AQI sensor."""
|
||||
super().__init__(air_purifier)
|
||||
self._attr_unique_id = f"{self._air_purifier.mac}-hourly-max-aqi"
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
"""Return the hourly max AQI value."""
|
||||
return self._air_purifier.max_hourly_aqi
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return device attributes of the entity."""
|
||||
attributes = super().extra_state_attributes
|
||||
attributes.update(
|
||||
{
|
||||
"hour_start": self._timestamp_attribute(
|
||||
self._air_purifier.max_hourly_aqi_start_time
|
||||
),
|
||||
"hour_end": self._timestamp_attribute(
|
||||
self._air_purifier.max_hourly_aqi_start_time,
|
||||
offset=datetime.timedelta(hours=1),
|
||||
),
|
||||
"sampled_until": self._timestamp_attribute(
|
||||
self._air_purifier.max_hourly_aqi_end_time
|
||||
),
|
||||
}
|
||||
)
|
||||
return attributes
|
||||
|
||||
@staticmethod
|
||||
def _timestamp_attribute(
|
||||
timestamp: int | None, offset: datetime.timedelta | None = None
|
||||
) -> str | None:
|
||||
"""Return an ISO formatted timestamp attribute."""
|
||||
if timestamp is None:
|
||||
return None
|
||||
|
||||
value = datetime.datetime.fromtimestamp(timestamp, datetime.UTC)
|
||||
if offset is not None:
|
||||
value += offset
|
||||
return value.isoformat()
|
||||
|
||||
+30
-30
@@ -4,7 +4,7 @@
|
||||
"state": "ON",
|
||||
"led_brightness": 100,
|
||||
"countdown_to_turn_off": 0,
|
||||
"voltage": 120.6,
|
||||
"voltage": 120.2,
|
||||
"countdown_to_turn_on": 0,
|
||||
"ac_frequency": 60,
|
||||
"power_factor": 0.11,
|
||||
@@ -22,15 +22,15 @@
|
||||
},
|
||||
"0xffffb40e0607af27": {
|
||||
"state": "ON",
|
||||
"voltage": 120.2,
|
||||
"voltage": 119.7,
|
||||
"ac_frequency": 60,
|
||||
"led_brightness": 100,
|
||||
"countdown_to_turn_off": 0,
|
||||
"countdown_to_turn_on": 0,
|
||||
"power": 3.9,
|
||||
"current": 0.13,
|
||||
"energy": 26.89,
|
||||
"power_factor": 0.25,
|
||||
"power": 96.8,
|
||||
"current": 2.53,
|
||||
"energy": 26.92,
|
||||
"power_factor": 0.32,
|
||||
"update": {
|
||||
"state": "idle",
|
||||
"installed_version": 268513381,
|
||||
@@ -45,10 +45,10 @@
|
||||
"state": "ON",
|
||||
"led_brightness": 100,
|
||||
"countdown_to_turn_off": 0,
|
||||
"voltage": 120.2,
|
||||
"voltage": 119.8,
|
||||
"countdown_to_turn_on": 0,
|
||||
"energy": 50.32,
|
||||
"power_factor": 0.89,
|
||||
"energy": 50.34,
|
||||
"power_factor": 0.2,
|
||||
"ac_frequency": 60,
|
||||
"update": {
|
||||
"state": "idle",
|
||||
@@ -58,8 +58,8 @@
|
||||
"latest_release_notes": null
|
||||
},
|
||||
"linkquality": 134,
|
||||
"power": 92.1,
|
||||
"current": 0.88,
|
||||
"power": 0.2,
|
||||
"current": 0.01,
|
||||
"power_on_behavior": "on"
|
||||
},
|
||||
"0xb40e060fffe031e3": {
|
||||
@@ -74,13 +74,13 @@
|
||||
"led_brightness": 100,
|
||||
"countdown_to_turn_off": 0,
|
||||
"countdown_to_turn_on": 0,
|
||||
"voltage": 119.3,
|
||||
"voltage": 118.8,
|
||||
"state": "ON",
|
||||
"ac_frequency": 60,
|
||||
"energy": 102.51,
|
||||
"power": 102.3,
|
||||
"current": 0.92,
|
||||
"power_factor": 0.95,
|
||||
"energy": 102.54,
|
||||
"power": 97.2,
|
||||
"current": 0.87,
|
||||
"power_factor": 0.94,
|
||||
"update": {
|
||||
"state": "idle",
|
||||
"installed_version": 268513381,
|
||||
@@ -95,11 +95,11 @@
|
||||
"led_brightness": 100,
|
||||
"countdown_to_turn_off": 0,
|
||||
"countdown_to_turn_on": 0,
|
||||
"voltage": 120.1,
|
||||
"energy": 43.84,
|
||||
"voltage": 119.5,
|
||||
"energy": 43.87,
|
||||
"state": "ON",
|
||||
"power": 79.1,
|
||||
"current": 0.83,
|
||||
"power": 84.2,
|
||||
"current": 0.91,
|
||||
"ac_frequency": 60,
|
||||
"power_factor": 0.76,
|
||||
"update": {
|
||||
@@ -114,12 +114,12 @@
|
||||
},
|
||||
"0xffffb40e060895b3": {
|
||||
"state": "ON",
|
||||
"voltage": 120.6,
|
||||
"voltage": 120.2,
|
||||
"ac_frequency": 60,
|
||||
"energy": 6.33,
|
||||
"current": 0.01,
|
||||
"power": 0.2,
|
||||
"power_factor": 0.13,
|
||||
"power_factor": 0.2,
|
||||
"linkquality": 123,
|
||||
"update": {
|
||||
"state": "idle",
|
||||
@@ -136,14 +136,14 @@
|
||||
"0xffffb40e0608864e": {
|
||||
"led_brightness": 100,
|
||||
"countdown_to_turn_off": 0,
|
||||
"voltage": 120.8,
|
||||
"voltage": 120.7,
|
||||
"energy": 17.34,
|
||||
"countdown_to_turn_on": 0,
|
||||
"state": "ON",
|
||||
"current": 0.02,
|
||||
"ac_frequency": 60,
|
||||
"power": 0.3,
|
||||
"power_factor": 0.2,
|
||||
"power": 0.4,
|
||||
"power_factor": 0.14,
|
||||
"update": {
|
||||
"state": "idle",
|
||||
"installed_version": 268513381,
|
||||
@@ -172,13 +172,13 @@
|
||||
"0xffffb40e060893d8": {
|
||||
"state": "ON",
|
||||
"led_brightness": 100,
|
||||
"voltage": 121.3,
|
||||
"voltage": 120.7,
|
||||
"countdown_to_turn_off": 0,
|
||||
"countdown_to_turn_on": 0,
|
||||
"energy": 3.04,
|
||||
"power_on_behavior": "on",
|
||||
"linkquality": 87,
|
||||
"current": 0.03,
|
||||
"current": 0.01,
|
||||
"ac_frequency": 60,
|
||||
"update": {
|
||||
"state": "idle",
|
||||
@@ -187,12 +187,12 @@
|
||||
"latest_source": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/SmartPlug_Zigbee_PROD_OTA_V101_1.01.01.ota",
|
||||
"latest_release_notes": null
|
||||
},
|
||||
"power_factor": 0.08,
|
||||
"power": 0.1
|
||||
"power_factor": 0.27,
|
||||
"power": 0.4
|
||||
},
|
||||
"0xa4c1380d0679ffff": {
|
||||
"battery": 100,
|
||||
"temperature": 27.5,
|
||||
"temperature": 27.6,
|
||||
"temperature_units": "celsius",
|
||||
"temperature_calibration": 0,
|
||||
"update": {
|
||||
|
||||
Reference in New Issue
Block a user