This commit is contained in:
Home Assistant Version Control
2026-08-07 16:23:20 +00:00
parent 305e1b2545
commit baabdf0c0e
58 changed files with 678 additions and 109 deletions
+7 -3
View File
@@ -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: 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 - `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 - `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 - `get_devices`, `get_areas` - Device and area registry info
- `write_config_safe` - **Safe config writing with automatic validation, content protection, and backup** - `write_config_safe` - **Safe config writing with automatic validation, content protection, and backup**
- `validate_config` - Check configuration validity - `validate_config` - Check configuration validity
- `get_error_log` - System errors and warnings - `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 - `diagnose_entity` - Comprehensive entity troubleshooting
- `get_agent_capabilities` - OpenCode MCP capabilities and native HA `llm` / MCP readiness - `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 - `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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | **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 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 | | **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 | | **Blueprints** | N/A | N/A | **`hab blueprint` (primary)** | N/A |
| **Update firmware** | N/A | **`watch_firmware_update`** | N/A | 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 | | **Check for updates** | N/A | `get_available_updates` | N/A | N/A |
File diff suppressed because one or more lines are too long
+9 -9
View File
@@ -212,7 +212,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
global_config = get_global_configuration(hass, config) 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] = { hass.data[DOMAIN] = {
DATA_DISCOVERY_MANAGER: discovery_manager, DATA_DISCOVERY_MANAGER: discovery_manager,
DOMAIN_CONFIG: global_config, DOMAIN_CONFIG: global_config,
@@ -225,6 +225,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
DATA_ANALYTICS: {}, DATA_ANALYTICS: {},
} }
await discovery_manager.setup()
register_services(hass) register_services(hass)
await async_load_platform(hass, Platform.SELECT, DOMAIN, {}, config) await async_load_platform(hass, Platform.SELECT, DOMAIN, {}, config)
@@ -235,8 +237,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
try: try:
await repair_none_config_entries_issue(hass) await repair_none_config_entries_issue(hass)
except Exception as e: # pragma: no cover except Exception: # pragma: no cover
_LOGGER.error("problem while cleaning up None entities", exc_info=e) # pragma: no cover _LOGGER.exception("problem while cleaning up None entities") # pragma: no cover
await init_analytics(hass) 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) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
async def create_discovery_manager_instance( def create_discovery_manager_instance(
hass: HomeAssistant, hass: HomeAssistant,
ha_config: ConfigType, ha_config: ConfigType,
global_powercalc_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) exclude_self_usage = discovery_config.get(CONF_EXCLUDE_SELF_USAGE, False)
enable_autodiscovery = discovery_config.get(CONF_ENABLED, True) enable_autodiscovery = discovery_config.get(CONF_ENABLED, True)
manager = DiscoveryManager( return DiscoveryManager(
hass, hass,
ha_config, ha_config,
exclude_device_types=exclude_device_types, exclude_device_types=exclude_device_types,
exclude_self_usage_profiles=exclude_self_usage, exclude_self_usage_profiles=exclude_self_usage,
enabled=enable_autodiscovery, enabled=enable_autodiscovery,
) )
await manager.setup()
return manager
def register_services(hass: HomeAssistant) -> None: 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) object.__setattr__(entry, "unique_id", unique_id)
hass.config_entries._entries._index_entry(entry) # noqa: SLF001 hass.config_entries._entries._index_entry(entry) # noqa: SLF001
await hass.config_entries.async_remove(entry.entry_id) await hass.config_entries.async_remove(entry.entry_id)
except Exception as e: # pragma: no cover except Exception: # pragma: no cover
_LOGGER.error("problem while cleaning up None entities", exc_info=e) # pragma: no cover _LOGGER.exception("problem while cleaning up None entities") # pragma: no cover
def _notify_message( def _notify_message(
+30 -7
View File
@@ -38,6 +38,7 @@ from .const import (
CONF_POWER, CONF_POWER,
CONF_SENSOR_TYPE, CONF_SENSOR_TYPE,
CONF_STANDBY_POWER, CONF_STANDBY_POWER,
DISCOVERY_INTEGRATION_NAME,
DISCOVERY_POWER_PROFILES, DISCOVERY_POWER_PROFILES,
DISCOVERY_SOURCE_ENTITY, DISCOVERY_SOURCE_ENTITY,
DOMAIN, DOMAIN,
@@ -400,6 +401,7 @@ class PowercalcConfigFlow(PowercalcCommonFlow, ConfigFlow, domain=DOMAIN):
self.source_entity_id = self.source_entity.entity_id self.source_entity_id = self.source_entity.entity_id
self.name = self.source_entity.name self.name = self.source_entity.name
integration_name = discovery_info.pop(DISCOVERY_INTEGRATION_NAME, None)
power_profiles: list[PowerProfile] = [] power_profiles: list[PowerProfile] = []
if DISCOVERY_POWER_PROFILES in discovery_info: if DISCOVERY_POWER_PROFILES in discovery_info:
@@ -410,7 +412,7 @@ class PowercalcConfigFlow(PowercalcCommonFlow, ConfigFlow, domain=DOMAIN):
self.sensor_config = discovery_info.copy() self.sensor_config = discovery_info.copy()
self.context["title_placeholders"] = { 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)), "manufacturer": str(self.sensor_config.get(CONF_MANUFACTURER)),
"model": str(self.sensor_config.get(CONF_MODEL)), "model": str(self.sensor_config.get(CONF_MODEL)),
} }
@@ -549,12 +551,7 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow):
menu = [Step.BASIC_OPTIONS] menu = [Step.BASIC_OPTIONS]
if self.selected_sensor_type == SensorType.VIRTUAL_POWER: if self.selected_sensor_type == SensorType.VIRTUAL_POWER:
if self.strategy and self.should_add_strategy_option_to_menu(): menu.extend(self.build_virtual_power_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)
if self.selected_sensor_type == SensorType.DAILY_ENERGY: if self.selected_sensor_type == SensorType.DAILY_ENERGY:
menu.append(Step.DAILY_ENERGY) menu.append(Step.DAILY_ENERGY)
if self.selected_sensor_type == SensorType.REAL_POWER: if self.selected_sensor_type == SensorType.REAL_POWER:
@@ -570,6 +567,32 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow):
return menu 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: def should_add_strategy_option_to_menu(self) -> bool:
"""Check whether the strategy option should be added to the menu.""" """Check whether the strategy option should be added to the menu."""
if not self.strategy or self.strategy not in STRATEGY_STEP_MAPPING: if not self.strategy or self.strategy not in STRATEGY_STEP_MAPPING:
+2
View File
@@ -240,7 +240,9 @@ DEFAULT_UTILITY_METER_TYPES = [DAILY, WEEKLY, MONTHLY]
DISCOVERY_SOURCE_ENTITY = "source_entity" DISCOVERY_SOURCE_ENTITY = "source_entity"
DISCOVERY_POWER_PROFILES = "power_profiles" DISCOVERY_POWER_PROFILES = "power_profiles"
DISCOVERY_INTEGRATION_NAME = "integration_name"
DISCOVERY_TYPE = "discovery_type" DISCOVERY_TYPE = "discovery_type"
LIBRARY_DISCOVERY_IGNORED_DOMAINS = "discovery_ignored_domains"
LIBRARY_URL = "https://library.powercalc.nl" LIBRARY_URL = "https://library.powercalc.nl"
API_URL = "https://api.powercalc.nl" API_URL = "https://api.powercalc.nl"
+61 -1
View File
@@ -31,6 +31,54 @@ def is_composite_device_id(hass: HomeAssistant, device_id: str) -> bool:
return bool(is_composite(device_id)) 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]: def get_config_entry_ids(device: DeviceEntry) -> set[str]:
""" """
Return the config entry IDs a device belongs to. 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: if _HAS_SINGLE_CONFIG_ENTRY:
return {device.config_entry_id} 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: 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( def attach_configured_device_entry(
hass: HomeAssistant, hass: HomeAssistant,
sensor_config: ConfigType, sensor_config: ConfigType,
+2 -2
View File
@@ -49,6 +49,6 @@ async def get_yaml_configuration(hass: HomeAssistant) -> ConfigType:
try: try:
yaml_config = await async_integration_yaml_config(hass, DOMAIN) yaml_config = await async_integration_yaml_config(hass, DOMAIN)
return yaml_config.get(DOMAIN, {}) # type: ignore return yaml_config.get(DOMAIN, {}) # type: ignore
except Exception as err: # noqa: BLE001 # pragma: nocover except Exception: # pragma: nocover
_LOGGER.error("Could not retrieve YAML config: %s", err) _LOGGER.exception("Could not retrieve YAML config")
return {} return {}
+97 -22
View File
@@ -1,4 +1,4 @@
from collections.abc import Awaitable, Callable from collections.abc import Callable
from datetime import datetime, timedelta from datetime import datetime, timedelta
from enum import StrEnum from enum import StrEnum
import logging import logging
@@ -8,14 +8,15 @@ from typing import Any, TypeVar
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, SOURCE_USER, ConfigEntry 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.const import CONF_DEVICE, CONF_ENTITY_ID, CONF_PLATFORM, CONF_UNIQUE_ID
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers import discovery_flow from homeassistant.helpers import discovery_flow
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
import homeassistant.helpers.entity_registry as er import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import IntegrationNotFound, async_get_integration
from .common import SourceEntity, create_source_entity from .common import SourceEntity, create_source_entity
from .const import ( from .const import (
@@ -24,6 +25,7 @@ from .const import (
CONF_MODEL, CONF_MODEL,
CONF_SENSORS, CONF_SENSORS,
DATA_DISCOVERY_MANAGER, DATA_DISCOVERY_MANAGER,
DISCOVERY_INTEGRATION_NAME,
DISCOVERY_POWER_PROFILES, DISCOVERY_POWER_PROFILES,
DISCOVERY_SOURCE_ENTITY, DISCOVERY_SOURCE_ENTITY,
DOMAIN, DOMAIN,
@@ -31,7 +33,12 @@ from .const import (
MANUFACTURER_WLED, MANUFACTURER_WLED,
CalculationStrategy, 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 ( from .group_include.filter import (
CategoryFilter, CategoryFilter,
CompositeFilter, CompositeFilter,
@@ -180,6 +187,8 @@ class DiscoveryManager:
continue # pragma: no cover continue # pragma: no cover
self.initialized_flows.add(entry.unique_id) self.initialized_flows.add(entry.unique_id)
self._initialize_configured_device(entry)
entity_id = entry.data.get(CONF_ENTITY_ID) entity_id = entry.data.get(CONF_ENTITY_ID)
if not entity_id or entity_id == DUMMY_ENTITY_ID: if not entity_id or entity_id == DUMMY_ENTITY_ID:
continue continue
@@ -189,6 +198,23 @@ class DiscoveryManager:
self.initialized_flows.add(f"pc_{entity.device_entry.id}") self.initialized_flows.add(f"pc_{entity.device_entry.id}")
self.initialized_flows.add(entity_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: def remove_initialized_flow(self, entry: ConfigEntry) -> None:
"""Remove a flow from the initialized flows.""" """Remove a flow from the initialized flows."""
if entry.unique_id: if entry.unique_id:
@@ -199,17 +225,22 @@ class DiscoveryManager:
async def perform_discovery( async def perform_discovery(
self, self,
source_provider: Callable[[], Awaitable[list[_DiscoverySourceT]]], source_provider: Callable[[], list[_DiscoverySourceT]],
source_creator: Callable[[_DiscoverySourceT], Awaitable[SourceEntity]], source_creator: Callable[[_DiscoverySourceT], SourceEntity],
discovery_type: DiscoveryBy, discovery_type: DiscoveryBy,
) -> None: ) -> None:
"""Generalized discovery procedure for entities and devices.""" """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( log_identifier = str(
getattr(source, "entity_id", getattr(source, "id", getattr(source, "entry_id", "unknown"))), getattr(source, "entity_id", getattr(source, "id", getattr(source, "entry_id", "unknown"))),
) )
try: 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( model_info = await self.extract_model_info_from_device_info(
source_entity.entity_entry or source_entity.device_entry, source_entity.entity_entry or source_entity.device_entry,
) )
@@ -235,13 +266,19 @@ class DiscoveryManager:
) )
continue continue
self._init_entity_discovery(model_info, unique_id, source_entity, log_identifier, power_profiles, {}) await self._init_entity_discovery(
except Exception as err: # noqa: BLE001 model_info,
_LOGGER.error( unique_id,
"%s: Error during %s discovery: %s", source_entity,
log_identifier,
power_profiles,
{},
)
except Exception:
_LOGGER.exception(
"%s: Error during %s discovery",
log_identifier, log_identifier,
discovery_type, discovery_type,
err,
) )
async def discover_entity( async def discover_entity(
@@ -256,12 +293,12 @@ class DiscoveryManager:
return await self.find_power_profiles(model_info, source_entity, discovery_type) 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.""" """Create SourceEntity for an entity."""
return create_source_entity(entity_entry.entity_id, self.hass) return create_source_entity(entity_entry.entity_id, self.hass)
@staticmethod @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.""" """Create SourceEntity for a device."""
return SourceEntity( return SourceEntity(
object_id=device_entry.name_by_user or device_entry.name or "", object_id=device_entry.name_by_user or device_entry.name or "",
@@ -271,7 +308,7 @@ class DiscoveryManager:
device_entry=device_entry, 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.""" """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) device_entry = get_first_device_for_config_entry(self.hass, config_entry.entry_id)
return SourceEntity( return SourceEntity(
@@ -341,6 +378,23 @@ class DiscoveryManager:
return power_profiles 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: async def init_wled_flow(self, model_info: ModelInfo, source_entity: SourceEntity) -> None:
"""Initialize the discovery flow for a WLED light.""" """Initialize the discovery flow for a WLED light."""
if DeviceType.LIGHT in self._exclude_device_types: if DeviceType.LIGHT in self._exclude_device_types:
@@ -358,7 +412,7 @@ class DiscoveryManager:
) )
return return
self._init_entity_discovery( await self._init_entity_discovery(
model_info, model_info,
unique_id, unique_id,
source_entity, source_entity,
@@ -379,7 +433,7 @@ class DiscoveryManager:
and not re.search("master|segment", str(entity_entry.entity_id), flags=re.IGNORECASE) 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.""" """Get all entities from entity registry which qualifies for discovery."""
def _check_already_configured(entity: er.RegistryEntry) -> bool: def _check_already_configured(entity: er.RegistryEntry) -> bool:
@@ -409,7 +463,7 @@ class DiscoveryManager:
) )
return get_filtered_entity_list(self.hass, NotFilter(entity_filter)) 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.""" """Fetch device entries."""
return [ return [
device device
@@ -417,7 +471,7 @@ class DiscoveryManager:
if not is_composite_device_id(self.hass, device.id) 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.""" """Fetch config entries which have at least one non-composite device."""
config_entry_ids = { config_entry_ids = {
config_entry_id config_entry_id
@@ -518,8 +572,7 @@ class DiscoveryManager:
return await self.get_model_information_from_device(device_entry) return await self.get_model_information_from_device(device_entry)
@callback async def _init_entity_discovery(
def _init_entity_discovery(
self, self,
model_info: ModelInfo, model_info: ModelInfo,
unique_id: str, unique_id: str,
@@ -532,6 +585,7 @@ class DiscoveryManager:
discovery_data: dict[str, Any] = { discovery_data: dict[str, Any] = {
CONF_ENTITY_ID: source_entity.entity_id, CONF_ENTITY_ID: source_entity.entity_id,
DISCOVERY_INTEGRATION_NAME: await self._get_integration_name(source_entity),
DISCOVERY_SOURCE_ENTITY: source_entity, DISCOVERY_SOURCE_ENTITY: source_entity,
CONF_UNIQUE_ID: unique_id, CONF_UNIQUE_ID: unique_id,
} }
@@ -564,6 +618,27 @@ class DiscoveryManager:
data=discovery_data, 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 @property
def status(self) -> DiscoveryStatus: def status(self) -> DiscoveryStatus:
"""Get the discovery status""" """Get the discovery status"""
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any
from homeassistant.config_entries import ConfigFlowResult from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_DEVICE from homeassistant.const import CONF_DEVICE
from homeassistant.helpers import selector, translation from homeassistant.helpers import selector, translation
from homeassistant.helpers.device_registry import DeviceEntry
import voluptuous as vol import voluptuous as vol
from custom_components.powercalc.const import ( from custom_components.powercalc.const import (
@@ -20,7 +21,7 @@ from custom_components.powercalc.const import (
LIBRARY_URL, LIBRARY_URL,
CalculationStrategy, 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 ( from custom_components.powercalc.discovery import (
get_power_profile_by_source_device, get_power_profile_by_source_device,
get_power_profile_by_source_entity, 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: 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.""" """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( return await self.flow.handle_form_step(
PowercalcFormStep( PowercalcFormStep(
step=Step.SELECT_DEVICE, step=Step.SELECT_DEVICE,
schema=vol.Schema( schema=self.build_select_device_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,
),
),
},
),
next_step=Step.POST_LIBRARY, next_step=Step.POST_LIBRARY,
), ),
user_input, 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: 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.""" """Return the next step needed to configure the calculation strategy, or None when nothing is left to ask."""
handled_steps = self.flow.handled_steps handled_steps = self.flow.handled_steps
@@ -641,6 +660,14 @@ class LibraryOptionsFlow(LibraryFlow):
super().__init__(flow) super().__init__(flow)
self.flow: PowercalcOptionsFlow = 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: async def async_step_library_options(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Handle the basic options flow.""" """Handle the basic options flow."""
self.flow.is_library_flow = True self.flow.is_library_flow = True
+1 -1
View File
@@ -22,5 +22,5 @@
"requirements": [ "requirements": [
"numpy>=1.21.1" "numpy>=1.21.1"
], ],
"version": "v1.24.0" "version": "v1.24.1"
} }
@@ -52,6 +52,11 @@ class ProfileLibrary:
async def initialize(self) -> None: async def initialize(self) -> None:
await self._loader.initialize() 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 @staticmethod
@singleton("powercalc_library") @singleton("powercalc_library")
async def factory(hass: HomeAssistant) -> ProfileLibrary: async def factory(hass: HomeAssistant) -> ProfileLibrary:
@@ -15,6 +15,10 @@ class CompositeLoader(Loader):
for loader in self.loaders: for loader in self.loaders:
await loader.initialize() 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( async def get_manufacturer_listing(
self, self,
device_types: set[DeviceType] | None, device_types: set[DeviceType] | None,
@@ -25,6 +25,10 @@ class LocalLoader(Loader):
if not self._is_custom_directory: if not self._is_custom_directory:
await self._hass.async_add_executor_job(self._load_custom_library) 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( async def get_manufacturer_listing(
self, self,
device_types: set[DeviceType] | None, device_types: set[DeviceType] | None,
@@ -7,6 +7,9 @@ class Loader(Protocol):
async def initialize(self) -> None: async def initialize(self) -> None:
"""Initialize the loader.""" """Initialize the loader."""
def get_discovery_ignored_domains(self) -> set[str]:
"""Get integration domains excluded from discovery."""
async def get_manufacturer_listing( async def get_manufacturer_listing(
self, self,
device_types: set[DeviceType] | None, 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.helpers.storage import STORAGE_DIR
from homeassistant.loader import async_get_integration 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.helpers import async_cache, clear_async_cache
from custom_components.powercalc.power_profile.error import LibraryLoadingError, ProfileDownloadError from custom_components.powercalc.power_profile.error import LibraryLoadingError, ProfileDownloadError
from custom_components.powercalc.power_profile.loader.protocol import Loader from custom_components.powercalc.power_profile.loader.protocol import Loader
@@ -80,6 +85,10 @@ class RemoteLoader(Loader):
for manufacturer in manufacturers: for manufacturer in manufacturers:
self._index_manufacturer(manufacturer, powercalc_version) 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: 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.""" """Register a manufacturer, its aliases and all of its supported models in the lookup tables."""
manufacturer_name = str(manufacturer.get("dir_name")) manufacturer_name = str(manufacturer.get("dir_name"))
+2 -5
View File
@@ -538,11 +538,8 @@ async def handle_nested_entity(
), ),
) )
entities_to_add.extend_items(child_entities) entities_to_add.extend_items(child_entities)
except SensorConfigurationError as exception: except SensorConfigurationError:
_LOGGER.error( _LOGGER.exception("Group state might be misbehaving because there was an error with an entity")
"Group state might be misbehaving because there was an error with an entity",
exc_info=exception,
)
async def add_discovered_entities( async def add_discovered_entities(
@@ -938,8 +938,8 @@ class PreviousStateStore:
instance.states[group] = { instance.states[group] = {
entity_id: State.from_dict(json_state) for (entity_id, json_state) in entities.items() entity_id: State.from_dict(json_state) for (entity_id, json_state) in entities.items()
} }
except HomeAssistantError as exc: # pragma: no cover except HomeAssistantError: # pragma: no cover
_LOGGER.error("Error loading previous energy sensor states", exc_info=exc) _LOGGER.exception("Error loading previous energy sensor states")
instance.async_setup_dump() instance.async_setup_dump()
@@ -983,8 +983,8 @@ class PreviousStateStore:
"""Save the current states to storage.""" """Save the current states to storage."""
try: try:
await self.store.async_save(self.states) await self.store.async_save(self.states)
except HomeAssistantError as exc: # pragma: no cover except HomeAssistantError: # pragma: no cover
_LOGGER.error("Error saving current states", exc_info=exc) _LOGGER.exception("Error saving current states")
@callback @callback
def async_setup_dump(self) -> None: def async_setup_dump(self) -> None:
@@ -971,6 +971,7 @@
"playbook": "Možnosti playbooku", "playbook": "Možnosti playbooku",
"multi_switch": "Možnosti vícenásobného spínače", "multi_switch": "Možnosti vícenásobného spínače",
"real_power": "Možnosti skutečného výkonu", "real_power": "Možnosti skutečného výkonu",
"select_device": "Zařízení",
"utility_meter_options": "Možnosti měřičů spotřeby", "utility_meter_options": "Možnosti měřičů spotřeby",
"wled": "Možnosti WLED", "wled": "Možnosti WLED",
"cost_options": "Možnosti nákladů" "cost_options": "Možnosti nákladů"
@@ -980,6 +981,16 @@
"title": "Možnosti knihovny", "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ší." "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": { "linear": {
"title": "Lineární možnosti", "title": "Lineární možnosti",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Playbook-indstillinger", "playbook": "Playbook-indstillinger",
"multi_switch": "Indstillinger for multikontakt", "multi_switch": "Indstillinger for multikontakt",
"real_power": "Indstillinger for fysisk effekt", "real_power": "Indstillinger for fysisk effekt",
"select_device": "Enhed",
"utility_meter_options": "Indstillinger for forbrugsmålere", "utility_meter_options": "Indstillinger for forbrugsmålere",
"wled": "WLED-indstillinger", "wled": "WLED-indstillinger",
"cost_options": "Omkostningsindstillinger" "cost_options": "Omkostningsindstillinger"
@@ -980,6 +981,16 @@
"title": "Biblioteksindstillinger", "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." "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": { "linear": {
"title": "Lineære indstillinger", "title": "Lineære indstillinger",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Playbook-Optionen", "playbook": "Playbook-Optionen",
"multi_switch": "Mehrfachschalter-Optionen", "multi_switch": "Mehrfachschalter-Optionen",
"real_power": "Optionen für reale Leistung", "real_power": "Optionen für reale Leistung",
"select_device": "Gerät",
"utility_meter_options": "Verbrauchszähler-Optionen", "utility_meter_options": "Verbrauchszähler-Optionen",
"wled": "WLED Einstellung", "wled": "WLED Einstellung",
"cost_options": "Kostenoptionen" "cost_options": "Kostenoptionen"
@@ -980,6 +981,16 @@
"title": "Bibliotheksoptionen", "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." "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": { "linear": {
"title": "Lineare Optionen", "title": "Lineare Optionen",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Playbook options", "playbook": "Playbook options",
"multi_switch": "Multi switch options", "multi_switch": "Multi switch options",
"real_power": "Real power options", "real_power": "Real power options",
"select_device": "Device",
"utility_meter_options": "Utility meter options", "utility_meter_options": "Utility meter options",
"wled": "WLED options", "wled": "WLED options",
"cost_options": "Cost options" "cost_options": "Cost options"
@@ -980,6 +981,16 @@
"title": "Library options", "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." "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": { "linear": {
"title": "Linear options", "title": "Linear options",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Opciones de Reproducciones", "playbook": "Opciones de Reproducciones",
"multi_switch": "Opciones multi-interruptor", "multi_switch": "Opciones multi-interruptor",
"real_power": "Opciones de potencia real", "real_power": "Opciones de potencia real",
"select_device": "Dispositivo",
"utility_meter_options": "Opciones de contador eléctrico", "utility_meter_options": "Opciones de contador eléctrico",
"wled": "Opciones de WLED", "wled": "Opciones de WLED",
"cost_options": "Opciones de costos" "cost_options": "Opciones de costos"
@@ -980,6 +981,16 @@
"title": "Opciones de biblioteca", "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." "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": { "linear": {
"title": "Opciones lineales", "title": "Opciones lineales",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Playbookin vaihtoehdot", "playbook": "Playbookin vaihtoehdot",
"multi_switch": "Useita kytkinvaihtoehtoja", "multi_switch": "Useita kytkinvaihtoehtoja",
"real_power": "Todelliset tehovaihtoehdot", "real_power": "Todelliset tehovaihtoehdot",
"select_device": "Laite",
"utility_meter_options": "Kulutusmittarin asetukset", "utility_meter_options": "Kulutusmittarin asetukset",
"wled": "WLED vaihtoehdot", "wled": "WLED vaihtoehdot",
"cost_options": "Kustannusvaihtoehdot" "cost_options": "Kustannusvaihtoehdot"
@@ -980,6 +981,16 @@
"title": "Kirjaston vaihtoehdot", "title": "Kirjaston vaihtoehdot",
"description": "Tällä hetkellä on valittuna seuraava kirjastoprofiili: \n valmistaja: {manufacturer}\n malli: {model}\n\nJos haluat vaihtaa profiilia, napsauta Seuraava." "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": { "linear": {
"title": "Lineaariset vaihtoehdot", "title": "Lineaariset vaihtoehdot",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Options du playbook", "playbook": "Options du playbook",
"multi_switch": "Options du multi-interrupteur", "multi_switch": "Options du multi-interrupteur",
"real_power": "Options de puissance réelle", "real_power": "Options de puissance réelle",
"select_device": "Appareil",
"utility_meter_options": "Options des compteurs de services publics", "utility_meter_options": "Options des compteurs de services publics",
"wled": "Options WLED", "wled": "Options WLED",
"cost_options": "Options de coût" "cost_options": "Options de coût"
@@ -980,6 +981,16 @@
"title": "Options de bibliothèque", "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." "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": "Lappareil auquel les entités Powercalc doivent être associées"
},
"description": "Sélectionnez lappareil auquel le capteur Powercalc doit être associé.",
"title": "Sélectionner un appareil"
},
"linear": { "linear": {
"title": "Options linéaires", "title": "Options linéaires",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Playbook beállításai", "playbook": "Playbook beállításai",
"multi_switch": "Többkapcsolós beállítások", "multi_switch": "Többkapcsolós beállítások",
"real_power": "Valós teljesítmény beállításai", "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", "utility_meter_options": "Közüzemi mérő beállításai",
"wled": "WLED beállításai", "wled": "WLED beállításai",
"cost_options": "Költségbeállítások" "cost_options": "Költségbeállítások"
@@ -980,6 +981,16 @@
"title": "Könyvtári beállítások", "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." "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": { "linear": {
"title": "Lineáris beállítások", "title": "Lineáris beállítások",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Opzioni del playbook", "playbook": "Opzioni del playbook",
"multi_switch": "Opzioni multiinterruttore", "multi_switch": "Opzioni multiinterruttore",
"real_power": "Opzioni di potenza reale", "real_power": "Opzioni di potenza reale",
"select_device": "Dispositivo",
"utility_meter_options": "Opzioni del contatore di utilità", "utility_meter_options": "Opzioni del contatore di utilità",
"wled": "Opzioni WLED", "wled": "Opzioni WLED",
"cost_options": "Opzioni di costo" "cost_options": "Opzioni di costo"
@@ -980,6 +981,16 @@
"title": "Opzioni della libreria", "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." "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": { "linear": {
"title": "Opzioni lineari", "title": "Opzioni lineari",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Playbook-alternativer", "playbook": "Playbook-alternativer",
"multi_switch": "Alternativer for flere brytere", "multi_switch": "Alternativer for flere brytere",
"real_power": "Innstillinger for målt effekt", "real_power": "Innstillinger for målt effekt",
"select_device": "Enhet",
"utility_meter_options": "Innstillinger for forbruksmåler", "utility_meter_options": "Innstillinger for forbruksmåler",
"wled": "WLED-innstillinger", "wled": "WLED-innstillinger",
"cost_options": "Kostnadsalternativer" "cost_options": "Kostnadsalternativer"
@@ -980,6 +981,16 @@
"title": "Alternativer for bibliotek", "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." "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": { "linear": {
"title": "Lineære alternativer", "title": "Lineære alternativer",
"data": { "data": {
@@ -10,7 +10,7 @@
"already_configured": "Sensor is reeds geconfigureerd, specifieer een uniek ID" "already_configured": "Sensor is reeds geconfigureerd, specifieer een uniek ID"
}, },
"error": { "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", "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", "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", "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", "playbook": "Playbook opties",
"multi_switch": "Multi switch opties", "multi_switch": "Multi switch opties",
"real_power": "Opties voor werkelijk vermogen", "real_power": "Opties voor werkelijk vermogen",
"select_device": "Apparaat",
"utility_meter_options": "Nutsmeter opties", "utility_meter_options": "Nutsmeter opties",
"wled": "WLED opties", "wled": "WLED opties",
"cost_options": "Kosten opties" "cost_options": "Kosten opties"
@@ -980,6 +981,16 @@
"title": "Bibliotheek opties", "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." "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": { "linear": {
"title": "Lineaire opties", "title": "Lineaire opties",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Opcje playbook", "playbook": "Opcje playbook",
"multi_switch": "Opcje wieloprzełącznika", "multi_switch": "Opcje wieloprzełącznika",
"real_power": "Opcje rzeczywistej mocy", "real_power": "Opcje rzeczywistej mocy",
"select_device": "Urządzenie",
"utility_meter_options": "Opcje licznika mediów", "utility_meter_options": "Opcje licznika mediów",
"wled": "Opcje WLED", "wled": "Opcje WLED",
"cost_options": "Opcje kosztów" "cost_options": "Opcje kosztów"
@@ -980,6 +981,16 @@
"title": "Opcje biblioteki", "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." "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": { "linear": {
"title": "Opcje liniowe", "title": "Opcje liniowe",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Opções de roteiro", "playbook": "Opções de roteiro",
"multi_switch": "Opções de multi-interruptor", "multi_switch": "Opções de multi-interruptor",
"real_power": "Opções de potência real", "real_power": "Opções de potência real",
"select_device": "Dispositivo",
"utility_meter_options": "Opções do medidor de utilidade", "utility_meter_options": "Opções do medidor de utilidade",
"wled": "Opções de WLED", "wled": "Opções de WLED",
"cost_options": "Opções de custo" "cost_options": "Opções de custo"
@@ -980,6 +981,16 @@
"title": "Opções da biblioteca", "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." "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": { "linear": {
"title": "Opções linear", "title": "Opções linear",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Opções de sequência", "playbook": "Opções de sequência",
"multi_switch": "Opções de interruptor múltiplo", "multi_switch": "Opções de interruptor múltiplo",
"real_power": "Opções de potência real", "real_power": "Opções de potência real",
"select_device": "Dispositivo",
"utility_meter_options": "Opções de contador de serviços", "utility_meter_options": "Opções de contador de serviços",
"wled": "Opções WLED", "wled": "Opções WLED",
"cost_options": "Opções de custo" "cost_options": "Opções de custo"
@@ -980,6 +981,16 @@
"title": "Opções da biblioteca", "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." "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": { "linear": {
"title": "Opções lineares", "title": "Opções lineares",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Opțiuni de playbook", "playbook": "Opțiuni de playbook",
"multi_switch": "Opțiuni de comutare multiplă", "multi_switch": "Opțiuni de comutare multiplă",
"real_power": "Opțiuni de putere reală", "real_power": "Opțiuni de putere reală",
"select_device": "Dispozitiv",
"utility_meter_options": "Opțiuni de contor de utilitate", "utility_meter_options": "Opțiuni de contor de utilitate",
"wled": "Opțiuni WLED", "wled": "Opțiuni WLED",
"cost_options": "Opțiuni de cost" "cost_options": "Opțiuni de cost"
@@ -980,6 +981,16 @@
"title": "Opțiuni de bibliotecă", "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." "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": { "linear": {
"title": "Opțiuni liniare", "title": "Opțiuni liniare",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Параметры сценариев", "playbook": "Параметры сценариев",
"multi_switch": "Параметры мультивыключателя", "multi_switch": "Параметры мультивыключателя",
"real_power": "Параметры реальной мощности", "real_power": "Параметры реальной мощности",
"select_device": "Устройство",
"utility_meter_options": "Параметры счётчиков", "utility_meter_options": "Параметры счётчиков",
"wled": "Параметры WLED", "wled": "Параметры WLED",
"cost_options": "Варианты стоимости" "cost_options": "Варианты стоимости"
@@ -980,6 +981,16 @@
"title": "Параметры библиотеки", "title": "Параметры библиотеки",
"description": "В настоящее время выбран следующий профиль библиотеки: \n производитель: {manufacturer}\n модель: {model}\n\nЕсли хотите изменить профиль, нажмите далее." "description": "В настоящее время выбран следующий профиль библиотеки: \n производитель: {manufacturer}\n модель: {model}\n\nЕсли хотите изменить профиль, нажмите далее."
}, },
"select_device": {
"data": {
"device": "Устройство"
},
"data_description": {
"device": "Устройство, к которому следует привязать сущности Powercalc"
},
"description": "Выберите устройство, к которому следует привязать датчик Powercalc.",
"title": "Выберите устройство"
},
"linear": { "linear": {
"title": "Линейные параметры", "title": "Линейные параметры",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Možnosti playbooku", "playbook": "Možnosti playbooku",
"multi_switch": "Možnosti viacerých prepínačov", "multi_switch": "Možnosti viacerých prepínačov",
"real_power": "Skutočné možnosti napájania", "real_power": "Skutočné možnosti napájania",
"select_device": "Zariadenie",
"utility_meter_options": "Možnosti elektromera", "utility_meter_options": "Možnosti elektromera",
"wled": "WLED možnosti", "wled": "WLED možnosti",
"cost_options": "Možnosti nákladov" "cost_options": "Možnosti nákladov"
@@ -980,6 +981,16 @@
"title": "Možnosti knižnice", "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." "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": { "linear": {
"title": "lineárne možnosti", "title": "lineárne možnosti",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "Playbook-alternativ", "playbook": "Playbook-alternativ",
"multi_switch": "Alternativ för flera switchar", "multi_switch": "Alternativ för flera switchar",
"real_power": "Verkliga kraftalternativ", "real_power": "Verkliga kraftalternativ",
"select_device": "Enhet",
"utility_meter_options": "Alternativ för verktygsmätare", "utility_meter_options": "Alternativ för verktygsmätare",
"wled": "WLED alternativ", "wled": "WLED alternativ",
"cost_options": "Kostnadsalternativ" "cost_options": "Kostnadsalternativ"
@@ -980,6 +981,16 @@
"title": "Biblioteksalternativ", "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." "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": { "linear": {
"title": "Linjära alternativ", "title": "Linjära alternativ",
"data": { "data": {
@@ -971,6 +971,7 @@
"playbook": "运行方案选项", "playbook": "运行方案选项",
"multi_switch": "多路开关选项", "multi_switch": "多路开关选项",
"real_power": "实际功率选项", "real_power": "实际功率选项",
"select_device": "设备",
"utility_meter_options": "公用事业计量表选项", "utility_meter_options": "公用事业计量表选项",
"wled": "WLED 选项", "wled": "WLED 选项",
"cost_options": "费用选项" "cost_options": "费用选项"
@@ -980,6 +981,16 @@
"title": "配置文件库选项", "title": "配置文件库选项",
"description": "当前选择了以下配置文件库中的配置文件:\n 制造商:{manufacturer}\n 型号:{model}\n\n如需更改配置文件,请点击“下一步”。" "description": "当前选择了以下配置文件库中的配置文件:\n 制造商:{manufacturer}\n 型号:{model}\n\n如需更改配置文件,请点击“下一步”。"
}, },
"select_device": {
"data": {
"device": "设备"
},
"data_description": {
"device": "Powercalc 实体应关联到的设备"
},
"description": "选择 Powercalc 传感器应关联到的设备。",
"title": "选择设备"
},
"linear": { "linear": {
"title": "线性选项", "title": "线性选项",
"data": { "data": {
+1
View File
@@ -34,6 +34,7 @@ from .token_manager import TokenManager
PLATFORMS = [ PLATFORMS = [
"light", "light",
"switch", "switch",
"fan",
"lock", "lock",
"climate", "climate",
"alarm_control_panel", "alarm_control_panel",
+1
View File
@@ -15,6 +15,7 @@ LOCK_UPDATED = f"{DOMAIN}.lock_updated"
CAMERA_UPDATED = f"{DOMAIN}.camera_updated" CAMERA_UPDATED = f"{DOMAIN}.camera_updated"
LIGHT_UPDATED = f"{DOMAIN}.light_updated" LIGHT_UPDATED = f"{DOMAIN}.light_updated"
COVER_UPDATED = f"{DOMAIN}.cover_updated" COVER_UPDATED = f"{DOMAIN}.cover_updated"
AIR_PURIFIER_UPDATED = f"{DOMAIN}.air_purifier_updated"
RESET_BUTTON_PRESSED = f"{DOMAIN}.reset_button_pressed" RESET_BUTTON_PRESSED = f"{DOMAIN}.reset_button_pressed"
# EVENT NAMES # EVENT NAMES
WYZE_CAMERA_EVENT = "wyze_camera_event" WYZE_CAMERA_EVENT = "wyze_camera_event"
+2 -2
View File
@@ -11,8 +11,8 @@
"issue_tracker": "https://github.com/SecKatie/ha-wyzeapi/issues", "issue_tracker": "https://github.com/SecKatie/ha-wyzeapi/issues",
"loggers": ["custom_components.wyzeapi"], "loggers": ["custom_components.wyzeapi"],
"requirements": [ "requirements": [
"wyzeapy>=0.5.33,<0.6", "wyzeapy>=0.6.1,<0.7",
"websockets" "websockets"
], ],
"version": "0.1.38" "version": "0.1.39"
} }
+145
View File
@@ -7,6 +7,7 @@ import logging
from typing import Any from typing import Any
from wyzeapy import Wyzeapy from wyzeapy import Wyzeapy
from wyzeapy.services.air_purifier_service import AirPurifier
from wyzeapy.services.camera_service import Camera from wyzeapy.services.camera_service import Camera
from wyzeapy.services.irrigation_service import Irrigation, IrrigationService from wyzeapy.services.irrigation_service import Irrigation, IrrigationService
from wyzeapy.services.lock_service import Lock from wyzeapy.services.lock_service import Lock
@@ -36,6 +37,7 @@ from homeassistant.helpers.event import (
) )
from .const import ( from .const import (
AIR_PURIFIER_UPDATED,
CAMERA_UPDATED, CAMERA_UPDATED,
CONF_CLIENT, CONF_CLIENT,
DOMAIN, DOMAIN,
@@ -71,6 +73,7 @@ async def async_setup_entry(
camera_service = await client.camera_service camera_service = await client.camera_service
switch_usage_service = await client.switch_usage_service switch_usage_service = await client.switch_usage_service
irrigation_service = await client.irrigation_service irrigation_service = await client.irrigation_service
air_purifier_service = await client.air_purifier_service
locks = await lock_service.get_locks() locks = await lock_service.get_locks()
sensors = [] sensors = []
@@ -95,6 +98,11 @@ async def async_setup_entry(
sensors.append(WyzePlugEnergySensor(plug, switch_usage_service)) sensors.append(WyzePlugEnergySensor(plug, switch_usage_service))
sensors.append(WyzePlugDailyEnergySensor(plug)) 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 # Get all irrigation devices
irrigation_devices = await irrigation_service.get_irrigations() irrigation_devices = await irrigation_service.get_irrigations()
@@ -624,3 +632,140 @@ class WyzeIrrigationSSID(WyzeIrrigationBaseSensor):
def native_value(self) -> str: def native_value(self) -> str:
"""Return the SSID.""" """Return the SSID."""
return self._device.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
View File
@@ -4,7 +4,7 @@
"state": "ON", "state": "ON",
"led_brightness": 100, "led_brightness": 100,
"countdown_to_turn_off": 0, "countdown_to_turn_off": 0,
"voltage": 120.6, "voltage": 120.2,
"countdown_to_turn_on": 0, "countdown_to_turn_on": 0,
"ac_frequency": 60, "ac_frequency": 60,
"power_factor": 0.11, "power_factor": 0.11,
@@ -22,15 +22,15 @@
}, },
"0xffffb40e0607af27": { "0xffffb40e0607af27": {
"state": "ON", "state": "ON",
"voltage": 120.2, "voltage": 119.7,
"ac_frequency": 60, "ac_frequency": 60,
"led_brightness": 100, "led_brightness": 100,
"countdown_to_turn_off": 0, "countdown_to_turn_off": 0,
"countdown_to_turn_on": 0, "countdown_to_turn_on": 0,
"power": 3.9, "power": 96.8,
"current": 0.13, "current": 2.53,
"energy": 26.89, "energy": 26.92,
"power_factor": 0.25, "power_factor": 0.32,
"update": { "update": {
"state": "idle", "state": "idle",
"installed_version": 268513381, "installed_version": 268513381,
@@ -45,10 +45,10 @@
"state": "ON", "state": "ON",
"led_brightness": 100, "led_brightness": 100,
"countdown_to_turn_off": 0, "countdown_to_turn_off": 0,
"voltage": 120.2, "voltage": 119.8,
"countdown_to_turn_on": 0, "countdown_to_turn_on": 0,
"energy": 50.32, "energy": 50.34,
"power_factor": 0.89, "power_factor": 0.2,
"ac_frequency": 60, "ac_frequency": 60,
"update": { "update": {
"state": "idle", "state": "idle",
@@ -58,8 +58,8 @@
"latest_release_notes": null "latest_release_notes": null
}, },
"linkquality": 134, "linkquality": 134,
"power": 92.1, "power": 0.2,
"current": 0.88, "current": 0.01,
"power_on_behavior": "on" "power_on_behavior": "on"
}, },
"0xb40e060fffe031e3": { "0xb40e060fffe031e3": {
@@ -74,13 +74,13 @@
"led_brightness": 100, "led_brightness": 100,
"countdown_to_turn_off": 0, "countdown_to_turn_off": 0,
"countdown_to_turn_on": 0, "countdown_to_turn_on": 0,
"voltage": 119.3, "voltage": 118.8,
"state": "ON", "state": "ON",
"ac_frequency": 60, "ac_frequency": 60,
"energy": 102.51, "energy": 102.54,
"power": 102.3, "power": 97.2,
"current": 0.92, "current": 0.87,
"power_factor": 0.95, "power_factor": 0.94,
"update": { "update": {
"state": "idle", "state": "idle",
"installed_version": 268513381, "installed_version": 268513381,
@@ -95,11 +95,11 @@
"led_brightness": 100, "led_brightness": 100,
"countdown_to_turn_off": 0, "countdown_to_turn_off": 0,
"countdown_to_turn_on": 0, "countdown_to_turn_on": 0,
"voltage": 120.1, "voltage": 119.5,
"energy": 43.84, "energy": 43.87,
"state": "ON", "state": "ON",
"power": 79.1, "power": 84.2,
"current": 0.83, "current": 0.91,
"ac_frequency": 60, "ac_frequency": 60,
"power_factor": 0.76, "power_factor": 0.76,
"update": { "update": {
@@ -114,12 +114,12 @@
}, },
"0xffffb40e060895b3": { "0xffffb40e060895b3": {
"state": "ON", "state": "ON",
"voltage": 120.6, "voltage": 120.2,
"ac_frequency": 60, "ac_frequency": 60,
"energy": 6.33, "energy": 6.33,
"current": 0.01, "current": 0.01,
"power": 0.2, "power": 0.2,
"power_factor": 0.13, "power_factor": 0.2,
"linkquality": 123, "linkquality": 123,
"update": { "update": {
"state": "idle", "state": "idle",
@@ -136,14 +136,14 @@
"0xffffb40e0608864e": { "0xffffb40e0608864e": {
"led_brightness": 100, "led_brightness": 100,
"countdown_to_turn_off": 0, "countdown_to_turn_off": 0,
"voltage": 120.8, "voltage": 120.7,
"energy": 17.34, "energy": 17.34,
"countdown_to_turn_on": 0, "countdown_to_turn_on": 0,
"state": "ON", "state": "ON",
"current": 0.02, "current": 0.02,
"ac_frequency": 60, "ac_frequency": 60,
"power": 0.3, "power": 0.4,
"power_factor": 0.2, "power_factor": 0.14,
"update": { "update": {
"state": "idle", "state": "idle",
"installed_version": 268513381, "installed_version": 268513381,
@@ -172,13 +172,13 @@
"0xffffb40e060893d8": { "0xffffb40e060893d8": {
"state": "ON", "state": "ON",
"led_brightness": 100, "led_brightness": 100,
"voltage": 121.3, "voltage": 120.7,
"countdown_to_turn_off": 0, "countdown_to_turn_off": 0,
"countdown_to_turn_on": 0, "countdown_to_turn_on": 0,
"energy": 3.04, "energy": 3.04,
"power_on_behavior": "on", "power_on_behavior": "on",
"linkquality": 87, "linkquality": 87,
"current": 0.03, "current": 0.01,
"ac_frequency": 60, "ac_frequency": 60,
"update": { "update": {
"state": "idle", "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_source": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/SmartPlug_Zigbee_PROD_OTA_V101_1.01.01.ota",
"latest_release_notes": null "latest_release_notes": null
}, },
"power_factor": 0.08, "power_factor": 0.27,
"power": 0.1 "power": 0.4
}, },
"0xa4c1380d0679ffff": { "0xa4c1380d0679ffff": {
"battery": 100, "battery": 100,
"temperature": 27.5, "temperature": 27.6,
"temperature_units": "celsius", "temperature_units": "celsius",
"temperature_calibration": 0, "temperature_calibration": 0,
"update": { "update": {