mirror of
https://github.com/home-assistant/core.git
synced 2026-01-11 17:48:37 +00:00
Compare commits
2 Commits
disable_py
...
knx-expose
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a54c29896 | ||
|
|
73a5e02b74 |
@@ -40,8 +40,7 @@
|
||||
"python.terminal.activateEnvInCurrentTerminal": true,
|
||||
"python.testing.pytestArgs": ["--no-cov"],
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
// Pyright type checking is too not compatible with mypy which Home Assistant uses for type checking
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
|
||||
4
.vscode/settings.default.jsonc
vendored
4
.vscode/settings.default.jsonc
vendored
@@ -7,8 +7,8 @@
|
||||
"python.testing.pytestEnabled": false,
|
||||
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
// Pyright type checking is too not compatible with mypy which Home Assistant uses for type checking
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
// Pyright is too pedantic for Home Assistant
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||
},
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -661,8 +661,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
|
||||
/homeassistant/components/hassio/ @home-assistant/supervisor
|
||||
/tests/components/hassio/ @home-assistant/supervisor
|
||||
/homeassistant/components/hdfury/ @glenndehaan
|
||||
/tests/components/hdfury/ @glenndehaan
|
||||
/homeassistant/components/hdmi_cec/ @inytar
|
||||
/tests/components/hdmi_cec/ @inytar
|
||||
/homeassistant/components/heatmiser/ @andylockran
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"""The HDFury Integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.SELECT,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: HDFuryConfigEntry) -> bool:
|
||||
"""Set up HDFury as config entry."""
|
||||
|
||||
coordinator = HDFuryCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: HDFuryConfigEntry) -> bool:
|
||||
"""Unload a HDFury config entry."""
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -1,54 +0,0 @@
|
||||
"""Config flow for HDFury Integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from hdfury import HDFuryAPI, HDFuryError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class HDFuryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle Config Flow for HDFury."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle Initial Setup."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
|
||||
serial = await self._validate_connection(host)
|
||||
if serial is not None:
|
||||
await self.async_set_unique_id(serial)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=f"HDFury ({host})", data=user_input
|
||||
)
|
||||
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _validate_connection(self, host: str) -> str | None:
|
||||
"""Try to fetch serial number to confirm it's a valid HDFury device."""
|
||||
|
||||
client = HDFuryAPI(host, async_get_clientsession(self.hass))
|
||||
|
||||
try:
|
||||
data = await client.get_board()
|
||||
except HDFuryError:
|
||||
return None
|
||||
|
||||
return data["serial"]
|
||||
@@ -1,3 +0,0 @@
|
||||
"""Constants for HDFury Integration."""
|
||||
|
||||
DOMAIN = "hdfury"
|
||||
@@ -1,67 +0,0 @@
|
||||
"""DataUpdateCoordinator for HDFury Integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from hdfury import HDFuryAPI, HDFuryError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL: Final = timedelta(seconds=60)
|
||||
|
||||
type HDFuryConfigEntry = ConfigEntry[HDFuryCoordinator]
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class HDFuryData:
|
||||
"""HDFury Data Class."""
|
||||
|
||||
board: dict[str, str]
|
||||
info: dict[str, str]
|
||||
config: dict[str, str]
|
||||
|
||||
|
||||
class HDFuryCoordinator(DataUpdateCoordinator[HDFuryData]):
|
||||
"""HDFury Device Coordinator Class."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: HDFuryConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name="HDFury",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.host: str = entry.data[CONF_HOST]
|
||||
self.client = HDFuryAPI(self.host, async_get_clientsession(hass))
|
||||
|
||||
async def _async_update_data(self) -> HDFuryData:
|
||||
"""Fetch the latest device data."""
|
||||
|
||||
try:
|
||||
board = await self.client.get_board()
|
||||
info = await self.client.get_info()
|
||||
config = await self.client.get_config()
|
||||
except HDFuryError as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
) from error
|
||||
|
||||
return HDFuryData(
|
||||
board=board,
|
||||
info=info,
|
||||
config=config,
|
||||
)
|
||||
@@ -1,39 +0,0 @@
|
||||
"""Base class for HDFury entities."""
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import HDFuryCoordinator
|
||||
|
||||
|
||||
class HDFuryEntity(CoordinatorEntity[HDFuryCoordinator]):
|
||||
"""Common elements for all entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, coordinator: HDFuryCoordinator, entity_description: EntityDescription
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = entity_description
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.data.board['serial']}_{entity_description.key}"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=f"HDFury {coordinator.data.board['hostname']}",
|
||||
manufacturer="HDFury",
|
||||
model=coordinator.data.board["hostname"].split("-")[0],
|
||||
serial_number=coordinator.data.board["serial"],
|
||||
sw_version=coordinator.data.board["version"].removeprefix("FW: "),
|
||||
hw_version=coordinator.data.board.get("pcbv"),
|
||||
configuration_url=f"http://{coordinator.host}",
|
||||
connections={
|
||||
(dr.CONNECTION_NETWORK_MAC, coordinator.data.config["macaddr"])
|
||||
},
|
||||
)
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"select": {
|
||||
"opmode": {
|
||||
"default": "mdi:cogs"
|
||||
},
|
||||
"portseltx0": {
|
||||
"default": "mdi:hdmi-port"
|
||||
},
|
||||
"portseltx1": {
|
||||
"default": "mdi:hdmi-port"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"domain": "hdfury",
|
||||
"name": "HDFury",
|
||||
"codeowners": ["@glenndehaan"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hdfury",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["hdfury==1.3.1"]
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: Integration does not register custom actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: Integration does not register custom actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: Entities do not explicitly subscribe to events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: Integration has no options flow.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: Integration has no authentication flow.
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: Device type integration.
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: Device type integration.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
||||
@@ -1,122 +0,0 @@
|
||||
"""Select platform for HDFury Integration."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from hdfury import (
|
||||
OPERATION_MODES,
|
||||
TX0_INPUT_PORTS,
|
||||
TX1_INPUT_PORTS,
|
||||
HDFuryAPI,
|
||||
HDFuryError,
|
||||
)
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
|
||||
from .entity import HDFuryEntity
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class HDFurySelectEntityDescription(SelectEntityDescription):
|
||||
"""Description for HDFury select entities."""
|
||||
|
||||
set_value_fn: Callable[[HDFuryAPI, str], Awaitable[None]]
|
||||
|
||||
|
||||
SELECT_PORTS: tuple[HDFurySelectEntityDescription, ...] = (
|
||||
HDFurySelectEntityDescription(
|
||||
key="portseltx0",
|
||||
translation_key="portseltx0",
|
||||
options=list(TX0_INPUT_PORTS.keys()),
|
||||
set_value_fn=lambda coordinator, value: _set_ports(coordinator),
|
||||
),
|
||||
HDFurySelectEntityDescription(
|
||||
key="portseltx1",
|
||||
translation_key="portseltx1",
|
||||
options=list(TX1_INPUT_PORTS.keys()),
|
||||
set_value_fn=lambda coordinator, value: _set_ports(coordinator),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
SELECT_OPERATION_MODE: HDFurySelectEntityDescription = HDFurySelectEntityDescription(
|
||||
key="opmode",
|
||||
translation_key="opmode",
|
||||
options=list(OPERATION_MODES.keys()),
|
||||
set_value_fn=lambda coordinator, value: coordinator.client.set_operation_mode(
|
||||
value
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def _set_ports(coordinator: HDFuryCoordinator) -> None:
|
||||
tx0 = coordinator.data.info.get("portseltx0")
|
||||
tx1 = coordinator.data.info.get("portseltx1")
|
||||
|
||||
if tx0 is None or tx1 is None:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="tx_state_error",
|
||||
translation_placeholders={"details": f"tx0={tx0}, tx1={tx1}"},
|
||||
)
|
||||
|
||||
await coordinator.client.set_port_selection(tx0, tx1)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HDFuryConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up selects using the platform schema."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities: list[HDFuryEntity] = []
|
||||
|
||||
for description in SELECT_PORTS:
|
||||
if description.key not in coordinator.data.info:
|
||||
continue
|
||||
|
||||
entities.append(HDFurySelect(coordinator, description))
|
||||
|
||||
# Add OPMODE select if present
|
||||
if "opmode" in coordinator.data.info:
|
||||
entities.append(HDFurySelect(coordinator, SELECT_OPERATION_MODE))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HDFurySelect(HDFuryEntity, SelectEntity):
|
||||
"""HDFury Select Class."""
|
||||
|
||||
entity_description: HDFurySelectEntityDescription
|
||||
|
||||
@property
|
||||
def current_option(self) -> str:
|
||||
"""Return the current option."""
|
||||
|
||||
return self.coordinator.data.info[self.entity_description.key]
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Update the current option."""
|
||||
|
||||
# Update local data first
|
||||
self.coordinator.data.info[self.entity_description.key] = option
|
||||
|
||||
# Send command to device
|
||||
try:
|
||||
await self.entity_description.set_value_fn(self.coordinator, option)
|
||||
except HDFuryError as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
) from error
|
||||
|
||||
# Trigger HA coordinator refresh
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP address of your HDFury device."
|
||||
},
|
||||
"description": "Set up your HDFury to integrate with Home Assistant."
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"select": {
|
||||
"opmode": {
|
||||
"name": "Operation mode",
|
||||
"state": {
|
||||
"0": "Mode 0 - Splitter TX0/TX1 FRL5 VRR",
|
||||
"1": "Mode 1 - Splitter TX0/TX1 UPSCALE FRL5",
|
||||
"2": "Mode 2 - Matrix TMDS",
|
||||
"3": "Mode 3 - Matrix FRL->TMDS",
|
||||
"4": "Mode 4 - Matrix DOWNSCALE",
|
||||
"5": "Mode 5 - Matrix RX0:FRL5 + RX1-3:TMDS"
|
||||
}
|
||||
},
|
||||
"portseltx0": {
|
||||
"name": "Port select TX0",
|
||||
"state": {
|
||||
"0": "Input 0",
|
||||
"1": "Input 1",
|
||||
"2": "Input 2",
|
||||
"3": "Input 3",
|
||||
"4": "Copy TX1"
|
||||
}
|
||||
},
|
||||
"portseltx1": {
|
||||
"name": "Port select TX1",
|
||||
"state": {
|
||||
"0": "Input 0",
|
||||
"1": "Input 1",
|
||||
"2": "Input 2",
|
||||
"3": "Input 3",
|
||||
"4": "Copy TX0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"communication_error": {
|
||||
"message": "An error occurred while communicating with HDFury device"
|
||||
},
|
||||
"tx_state_error": {
|
||||
"message": "An error occurred while validating TX states: {details}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,6 @@ set_program_and_options:
|
||||
- dishcare_dishwasher_program_intensiv_45
|
||||
- dishcare_dishwasher_program_auto_half_load
|
||||
- dishcare_dishwasher_program_intensiv_power
|
||||
- dishcare_dishwasher_program_intensive_fixed_zone
|
||||
- dishcare_dishwasher_program_magic_daily
|
||||
- dishcare_dishwasher_program_super_60
|
||||
- dishcare_dishwasher_program_kurz_60
|
||||
|
||||
@@ -272,7 +272,6 @@
|
||||
"dishcare_dishwasher_program_intensiv_45": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_45%]",
|
||||
"dishcare_dishwasher_program_intensiv_70": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_70%]",
|
||||
"dishcare_dishwasher_program_intensiv_power": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_power%]",
|
||||
"dishcare_dishwasher_program_intensive_fixed_zone": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensive_fixed_zone%]",
|
||||
"dishcare_dishwasher_program_kurz_60": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_kurz_60%]",
|
||||
"dishcare_dishwasher_program_learning_dishwasher": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_learning_dishwasher%]",
|
||||
"dishcare_dishwasher_program_machine_care": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_machine_care%]",
|
||||
@@ -627,7 +626,6 @@
|
||||
"dishcare_dishwasher_program_intensiv_45": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_45%]",
|
||||
"dishcare_dishwasher_program_intensiv_70": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_70%]",
|
||||
"dishcare_dishwasher_program_intensiv_power": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_power%]",
|
||||
"dishcare_dishwasher_program_intensive_fixed_zone": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensive_fixed_zone%]",
|
||||
"dishcare_dishwasher_program_kurz_60": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_kurz_60%]",
|
||||
"dishcare_dishwasher_program_learning_dishwasher": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_learning_dishwasher%]",
|
||||
"dishcare_dishwasher_program_machine_care": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_machine_care%]",
|
||||
@@ -1621,7 +1619,6 @@
|
||||
"dishcare_dishwasher_program_intensiv_45": "Intensive 45ºC",
|
||||
"dishcare_dishwasher_program_intensiv_70": "Intensive 70ºC",
|
||||
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
|
||||
"dishcare_dishwasher_program_intensive_fixed_zone": "Intensive fixed zone",
|
||||
"dishcare_dishwasher_program_kurz_60": "Speed 60ºC",
|
||||
"dishcare_dishwasher_program_learning_dishwasher": "Intelligent",
|
||||
"dishcare_dishwasher_program_machine_care": "Machine care",
|
||||
|
||||
@@ -27,7 +27,7 @@ from .const import (
|
||||
SUPPORTED_PLATFORMS_UI,
|
||||
SUPPORTED_PLATFORMS_YAML,
|
||||
)
|
||||
from .expose import create_knx_exposure
|
||||
from .expose import create_combined_knx_exposure
|
||||
from .knx_module import KNXModule
|
||||
from .project import STORAGE_KEY as PROJECT_STORAGE_KEY
|
||||
from .schema import (
|
||||
@@ -121,10 +121,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass.data[KNX_MODULE_KEY] = knx_module
|
||||
|
||||
if CONF_KNX_EXPOSE in config:
|
||||
for expose_config in config[CONF_KNX_EXPOSE]:
|
||||
knx_module.exposures.append(
|
||||
create_knx_exposure(hass, knx_module.xknx, expose_config)
|
||||
)
|
||||
knx_module.yaml_exposures.extend(
|
||||
create_combined_knx_exposure(hass, knx_module.xknx, config[CONF_KNX_EXPOSE])
|
||||
)
|
||||
|
||||
configured_platforms_yaml = {
|
||||
platform for platform in SUPPORTED_PLATFORMS_YAML if platform in config
|
||||
}
|
||||
@@ -149,7 +149,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# if not loaded directly return
|
||||
return True
|
||||
|
||||
for exposure in knx_module.exposures:
|
||||
for exposure in knx_module.yaml_exposures:
|
||||
exposure.async_remove()
|
||||
for exposure in knx_module.service_exposures.values():
|
||||
exposure.async_remove()
|
||||
|
||||
configured_platforms_yaml = {
|
||||
|
||||
@@ -2,14 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Iterable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from xknx import XKNX
|
||||
from xknx.devices import DateDevice, DateTimeDevice, ExposeSensor, TimeDevice
|
||||
from xknx.dpt import DPTNumeric, DPTString
|
||||
from xknx.dpt import DPTBase, DPTNumeric, DPTString
|
||||
from xknx.dpt.dpt_1 import DPT1BitEnum, DPTSwitch
|
||||
from xknx.exceptions import ConversionError
|
||||
from xknx.remote_value import RemoteValueSensor
|
||||
from xknx.telegram.address import (
|
||||
GroupAddress,
|
||||
InternalGroupAddress,
|
||||
parse_device_group_address,
|
||||
)
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_ID,
|
||||
@@ -41,79 +48,159 @@ _LOGGER = logging.getLogger(__name__)
|
||||
@callback
|
||||
def create_knx_exposure(
|
||||
hass: HomeAssistant, xknx: XKNX, config: ConfigType
|
||||
) -> KNXExposeSensor | KNXExposeTime:
|
||||
"""Create exposures from config."""
|
||||
|
||||
) -> KnxExposeEntity | KnxExposeTime:
|
||||
"""Create single exposure."""
|
||||
expose_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
|
||||
|
||||
exposure: KNXExposeSensor | KNXExposeTime
|
||||
exposure: KnxExposeEntity | KnxExposeTime
|
||||
if (
|
||||
isinstance(expose_type, str)
|
||||
and expose_type.lower() in ExposeSchema.EXPOSE_TIME_TYPES
|
||||
):
|
||||
exposure = KNXExposeTime(
|
||||
exposure = KnxExposeTime(
|
||||
xknx=xknx,
|
||||
config=config,
|
||||
)
|
||||
else:
|
||||
exposure = KNXExposeSensor(
|
||||
hass,
|
||||
exposure = KnxExposeEntity(
|
||||
hass=hass,
|
||||
xknx=xknx,
|
||||
config=config,
|
||||
entity_id=config[CONF_ENTITY_ID],
|
||||
options=(_yaml_config_to_expose_options(config),),
|
||||
)
|
||||
exposure.async_register()
|
||||
return exposure
|
||||
|
||||
|
||||
class KNXExposeSensor:
|
||||
"""Object to Expose Home Assistant entity to KNX bus."""
|
||||
@callback
|
||||
def create_combined_knx_exposure(
|
||||
hass: HomeAssistant, xknx: XKNX, configs: list[ConfigType]
|
||||
) -> list[KnxExposeEntity | KnxExposeTime]:
|
||||
"""Create exposures from YAML config combined by entity_id."""
|
||||
exposures: list[KnxExposeEntity | KnxExposeTime] = []
|
||||
entity_exposure_map: dict[str, list[KnxExposeOptions]] = {}
|
||||
|
||||
for config in configs:
|
||||
value_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
|
||||
if value_type.lower() in ExposeSchema.EXPOSE_TIME_TYPES:
|
||||
time_exposure = KnxExposeTime(
|
||||
xknx=xknx,
|
||||
config=config,
|
||||
)
|
||||
time_exposure.async_register()
|
||||
exposures.append(time_exposure)
|
||||
continue
|
||||
|
||||
entity_id = config[CONF_ENTITY_ID]
|
||||
option = _yaml_config_to_expose_options(config)
|
||||
entity_exposure_map.setdefault(entity_id, []).append(option)
|
||||
|
||||
for entity_id, options in entity_exposure_map.items():
|
||||
entity_exposure = KnxExposeEntity(
|
||||
hass=hass,
|
||||
xknx=xknx,
|
||||
entity_id=entity_id,
|
||||
options=options,
|
||||
)
|
||||
entity_exposure.async_register()
|
||||
exposures.append(entity_exposure)
|
||||
return exposures
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class KnxExposeOptions:
|
||||
"""Options for KNX Expose."""
|
||||
|
||||
attribute: str | None
|
||||
group_address: GroupAddress | InternalGroupAddress
|
||||
dpt: type[DPTBase]
|
||||
respond_to_read: bool
|
||||
cooldown: float
|
||||
default: Any | None
|
||||
value_template: Template | None
|
||||
|
||||
|
||||
def _yaml_config_to_expose_options(config: ConfigType) -> KnxExposeOptions:
|
||||
"""Convert single yaml expose config to KnxExposeOptions."""
|
||||
value_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
|
||||
dpt: type[DPTBase]
|
||||
if value_type == "binary":
|
||||
# HA yaml expose flag for DPT-1 (no explicit DPT 1 definitions in xknx back then)
|
||||
dpt = DPTSwitch
|
||||
else:
|
||||
dpt = DPTBase.parse_transcoder(config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]) # type: ignore[assignment] # checked by schema validation
|
||||
ga = parse_device_group_address(config[KNX_ADDRESS])
|
||||
return KnxExposeOptions(
|
||||
attribute=config.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE),
|
||||
group_address=ga,
|
||||
dpt=dpt,
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
cooldown=config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN],
|
||||
default=config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT),
|
||||
value_template=config.get(CONF_VALUE_TEMPLATE),
|
||||
)
|
||||
|
||||
|
||||
class KnxExposeEntity:
|
||||
"""Expose Home Assistant entity values to KNX bus."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
xknx: XKNX,
|
||||
config: ConfigType,
|
||||
entity_id: str,
|
||||
options: Iterable[KnxExposeOptions],
|
||||
) -> None:
|
||||
"""Initialize of Expose class."""
|
||||
"""Initialize KnxExposeEntity class."""
|
||||
self.hass = hass
|
||||
self.xknx = xknx
|
||||
|
||||
self.entity_id: str = config[CONF_ENTITY_ID]
|
||||
self.expose_attribute: str | None = config.get(
|
||||
ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE
|
||||
)
|
||||
self.expose_default = config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT)
|
||||
self.expose_type: int | str = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
|
||||
self.value_template: Template | None = config.get(CONF_VALUE_TEMPLATE)
|
||||
self.entity_id = entity_id
|
||||
|
||||
self._remove_listener: Callable[[], None] | None = None
|
||||
self.device: ExposeSensor = ExposeSensor(
|
||||
xknx=self.xknx,
|
||||
name=f"{self.entity_id}__{self.expose_attribute or 'state'}",
|
||||
group_address=config[KNX_ADDRESS],
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
value_type=self.expose_type,
|
||||
cooldown=config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN],
|
||||
self._exposures = tuple(
|
||||
(
|
||||
option,
|
||||
ExposeSensor(
|
||||
xknx=self.xknx,
|
||||
name=f"{self.entity_id} {option.attribute or 'state'}",
|
||||
group_address=option.group_address,
|
||||
respond_to_read=option.respond_to_read,
|
||||
value_type=option.dpt,
|
||||
cooldown=option.cooldown,
|
||||
),
|
||||
)
|
||||
for option in options
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return name of the expose entity."""
|
||||
expose_names = [opt.attribute or "state" for opt, _ in self._exposures]
|
||||
return f"{self.entity_id}__{'__'.join(expose_names)}"
|
||||
|
||||
@callback
|
||||
def async_register(self) -> None:
|
||||
"""Register listener."""
|
||||
"""Register listener and XKNX devices."""
|
||||
self._remove_listener = async_track_state_change_event(
|
||||
self.hass, [self.entity_id], self._async_entity_changed
|
||||
)
|
||||
self.xknx.devices.async_add(self.device)
|
||||
for _option, xknx_expose in self._exposures:
|
||||
self.xknx.devices.async_add(xknx_expose)
|
||||
self._init_expose_state()
|
||||
|
||||
@callback
|
||||
def _init_expose_state(self) -> None:
|
||||
"""Initialize state of the exposure."""
|
||||
"""Initialize state of all exposures."""
|
||||
init_state = self.hass.states.get(self.entity_id)
|
||||
state_value = self._get_expose_value(init_state)
|
||||
try:
|
||||
self.device.sensor_value.value = state_value
|
||||
except ConversionError:
|
||||
_LOGGER.exception("Error during sending of expose sensor value")
|
||||
for option, xknx_expose in self._exposures:
|
||||
state_value = self._get_expose_value(init_state, option)
|
||||
try:
|
||||
xknx_expose.sensor_value.value = state_value
|
||||
except ConversionError:
|
||||
_LOGGER.exception(
|
||||
"Error setting value %s for expose sensor %s",
|
||||
state_value,
|
||||
xknx_expose.name,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_remove(self) -> None:
|
||||
@@ -121,53 +208,57 @@ class KNXExposeSensor:
|
||||
if self._remove_listener is not None:
|
||||
self._remove_listener()
|
||||
self._remove_listener = None
|
||||
self.xknx.devices.async_remove(self.device)
|
||||
for _option, xknx_expose in self._exposures:
|
||||
self.xknx.devices.async_remove(xknx_expose)
|
||||
|
||||
def _get_expose_value(self, state: State | None) -> bool | int | float | str | None:
|
||||
"""Extract value from state."""
|
||||
def _get_expose_value(
|
||||
self, state: State | None, option: KnxExposeOptions
|
||||
) -> bool | int | float | str | None:
|
||||
"""Extract value from state for a specific option."""
|
||||
if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
|
||||
if self.expose_default is None:
|
||||
if option.default is None:
|
||||
return None
|
||||
value = self.expose_default
|
||||
elif self.expose_attribute is not None:
|
||||
_attr = state.attributes.get(self.expose_attribute)
|
||||
value = _attr if _attr is not None else self.expose_default
|
||||
value = option.default
|
||||
elif option.attribute is not None:
|
||||
_attr = state.attributes.get(option.attribute)
|
||||
value = _attr if _attr is not None else option.default
|
||||
else:
|
||||
value = state.state
|
||||
|
||||
if self.value_template is not None:
|
||||
if option.value_template is not None:
|
||||
try:
|
||||
value = self.value_template.async_render_with_possible_json_value(
|
||||
value = option.value_template.async_render_with_possible_json_value(
|
||||
value, error_value=None
|
||||
)
|
||||
except (TemplateError, TypeError, ValueError) as err:
|
||||
_LOGGER.warning(
|
||||
"Error rendering value template for KNX expose %s %s: %s",
|
||||
self.device.name,
|
||||
self.value_template.template,
|
||||
"Error rendering value template for KNX expose %s %s %s: %s",
|
||||
self.entity_id,
|
||||
option.attribute or "state",
|
||||
option.value_template.template,
|
||||
err,
|
||||
)
|
||||
return None
|
||||
|
||||
if self.expose_type == "binary":
|
||||
if issubclass(option.dpt, DPT1BitEnum):
|
||||
if value in (1, STATE_ON, "True"):
|
||||
return True
|
||||
if value in (0, STATE_OFF, "False"):
|
||||
return False
|
||||
if value is not None and (
|
||||
isinstance(self.device.sensor_value, RemoteValueSensor)
|
||||
):
|
||||
|
||||
# Handle numeric and string DPT conversions
|
||||
if value is not None:
|
||||
try:
|
||||
if issubclass(self.device.sensor_value.dpt_class, DPTNumeric):
|
||||
if issubclass(option.dpt, DPTNumeric):
|
||||
return float(value)
|
||||
if issubclass(self.device.sensor_value.dpt_class, DPTString):
|
||||
if issubclass(option.dpt, DPTString):
|
||||
# DPT 16.000 only allows up to 14 Bytes
|
||||
return str(value)[:14]
|
||||
except (ValueError, TypeError) as err:
|
||||
_LOGGER.warning(
|
||||
'Could not expose %s %s value "%s" to KNX: Conversion failed: %s',
|
||||
self.entity_id,
|
||||
self.expose_attribute or "state",
|
||||
option.attribute or "state",
|
||||
value,
|
||||
err,
|
||||
)
|
||||
@@ -175,32 +266,40 @@ class KNXExposeSensor:
|
||||
return value # type: ignore[no-any-return]
|
||||
|
||||
async def _async_entity_changed(self, event: Event[EventStateChangedData]) -> None:
|
||||
"""Handle entity change."""
|
||||
"""Handle entity change for all options."""
|
||||
new_state = event.data["new_state"]
|
||||
if (new_value := self._get_expose_value(new_state)) is None:
|
||||
return
|
||||
old_state = event.data["old_state"]
|
||||
# don't use default value for comparison on first state change (old_state is None)
|
||||
old_value = self._get_expose_value(old_state) if old_state is not None else None
|
||||
# don't send same value sequentially
|
||||
if new_value != old_value:
|
||||
await self._async_set_knx_value(new_value)
|
||||
|
||||
async def _async_set_knx_value(self, value: StateType) -> None:
|
||||
for option, xknx_expose in self._exposures:
|
||||
new_value = self._get_expose_value(new_state, option)
|
||||
if new_value is None:
|
||||
continue
|
||||
# Don't use default value for comparison on first state change
|
||||
old_value = (
|
||||
self._get_expose_value(old_state, option)
|
||||
if old_state is not None
|
||||
else None
|
||||
)
|
||||
# Don't send same value sequentially
|
||||
if new_value != old_value:
|
||||
await self._async_set_knx_value(xknx_expose, new_value)
|
||||
|
||||
async def _async_set_knx_value(
|
||||
self, xknx_expose: ExposeSensor, value: StateType
|
||||
) -> None:
|
||||
"""Set new value on xknx ExposeSensor."""
|
||||
try:
|
||||
await self.device.set(value)
|
||||
await xknx_expose.set(value)
|
||||
except ConversionError as err:
|
||||
_LOGGER.warning(
|
||||
'Could not expose %s %s value "%s" to KNX: %s',
|
||||
self.entity_id,
|
||||
self.expose_attribute or "state",
|
||||
'Could not expose %s value "%s" to KNX: %s',
|
||||
xknx_expose.name,
|
||||
value,
|
||||
err,
|
||||
)
|
||||
|
||||
|
||||
class KNXExposeTime:
|
||||
class KnxExposeTime:
|
||||
"""Object to Expose Time/Date object to KNX bus."""
|
||||
|
||||
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
|
||||
@@ -222,6 +321,11 @@ class KNXExposeTime:
|
||||
group_address=config[KNX_ADDRESS],
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return name of the time expose object."""
|
||||
return f"expose_{self.device.name}"
|
||||
|
||||
@callback
|
||||
def async_register(self) -> None:
|
||||
"""Register listener."""
|
||||
|
||||
@@ -54,7 +54,7 @@ from .const import (
|
||||
TELEGRAM_LOG_DEFAULT,
|
||||
)
|
||||
from .device import KNXInterfaceDevice
|
||||
from .expose import KNXExposeSensor, KNXExposeTime
|
||||
from .expose import KnxExposeEntity, KnxExposeTime
|
||||
from .project import KNXProject
|
||||
from .repairs import data_secure_group_key_issue_dispatcher
|
||||
from .storage.config_store import KNXConfigStore
|
||||
@@ -73,8 +73,8 @@ class KNXModule:
|
||||
self.hass = hass
|
||||
self.config_yaml = config
|
||||
self.connected = False
|
||||
self.exposures: list[KNXExposeSensor | KNXExposeTime] = []
|
||||
self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {}
|
||||
self.yaml_exposures: list[KnxExposeEntity | KnxExposeTime] = []
|
||||
self.service_exposures: dict[str, KnxExposeEntity | KnxExposeTime] = {}
|
||||
self.entry = entry
|
||||
|
||||
self.project = KNXProject(hass=hass, entry=entry)
|
||||
|
||||
@@ -193,7 +193,7 @@ async def service_exposure_register_modify(call: ServiceCall) -> None:
|
||||
" for '%s' - %s"
|
||||
),
|
||||
group_address,
|
||||
replaced_exposure.device.name,
|
||||
replaced_exposure.name,
|
||||
)
|
||||
replaced_exposure.async_remove()
|
||||
exposure = create_knx_exposure(knx_module.hass, knx_module.xknx, call.data)
|
||||
@@ -201,7 +201,7 @@ async def service_exposure_register_modify(call: ServiceCall) -> None:
|
||||
_LOGGER.debug(
|
||||
"Service exposure_register registered exposure for '%s' - %s",
|
||||
group_address,
|
||||
exposure.device.name,
|
||||
exposure.name,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
"""Support for Netatmo binary sensors."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Final, cast
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
@@ -13,33 +9,17 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import NETATMO_CREATE_WEATHER_BINARY_SENSOR
|
||||
from .const import NETATMO_CREATE_WEATHER_SENSOR
|
||||
from .data_handler import NetatmoDevice
|
||||
from .entity import NetatmoWeatherModuleEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class NetatmoBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes Netatmo binary sensor entity."""
|
||||
|
||||
name: str | None = None # The default name of the sensor
|
||||
netatmo_name: str # The name used by Netatmo API for this sensor
|
||||
|
||||
|
||||
NETATMO_WEATHER_BINARY_SENSOR_DESCRIPTIONS: Final[
|
||||
list[NetatmoBinarySensorEntityDescription]
|
||||
] = [
|
||||
NetatmoBinarySensorEntityDescription(
|
||||
BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
|
||||
BinarySensorEntityDescription(
|
||||
key="reachable",
|
||||
name="Connectivity",
|
||||
netatmo_name="reachable",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -47,75 +27,36 @@ async def async_setup_entry(
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Netatmo weather binary sensors based on a config entry."""
|
||||
"""Set up Netatmo binary sensors based on a config entry."""
|
||||
|
||||
@callback
|
||||
def _create_weather_binary_sensor_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
"""Create weather binary sensor entities for a Netatmo weather device."""
|
||||
|
||||
descriptions_to_add = NETATMO_WEATHER_BINARY_SENSOR_DESCRIPTIONS
|
||||
|
||||
entities: list[NetatmoWeatherBinarySensor] = []
|
||||
|
||||
# Create binary sensors for module
|
||||
for description in descriptions_to_add:
|
||||
# Actual check is simple for reachable
|
||||
feature_check = description.key
|
||||
if feature_check in netatmo_device.device.features:
|
||||
_LOGGER.debug(
|
||||
'Adding "%s" weather binary sensor for device %s',
|
||||
feature_check,
|
||||
netatmo_device.device.name,
|
||||
)
|
||||
entities.append(
|
||||
NetatmoWeatherBinarySensor(
|
||||
netatmo_device,
|
||||
description,
|
||||
)
|
||||
)
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
async_add_entities(
|
||||
NetatmoWeatherBinarySensor(netatmo_device, description)
|
||||
for description in BINARY_SENSOR_TYPES
|
||||
if description.key in netatmo_device.device.features
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
|
||||
_create_weather_binary_sensor_entity,
|
||||
hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_binary_sensor_entity
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class NetatmoWeatherBinarySensor(NetatmoWeatherModuleEntity, BinarySensorEntity):
|
||||
"""Implementation of a Netatmo weather binary sensor."""
|
||||
|
||||
entity_description: NetatmoBinarySensorEntityDescription
|
||||
"""Implementation of a Netatmo binary sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
netatmo_device: NetatmoDevice,
|
||||
description: NetatmoBinarySensorEntityDescription,
|
||||
self, device: NetatmoDevice, description: BinarySensorEntityDescription
|
||||
) -> None:
|
||||
"""Initialize a Netatmo weather binary sensor."""
|
||||
|
||||
super().__init__(netatmo_device)
|
||||
|
||||
"""Initialize a Netatmo binary sensor."""
|
||||
super().__init__(device)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{self.device.entity_id}-{description.key}"
|
||||
|
||||
@callback
|
||||
def async_update_callback(self) -> None:
|
||||
"""Update the entity's state."""
|
||||
|
||||
value: StateType | None = None
|
||||
|
||||
value = getattr(self.device, self.entity_description.netatmo_name, None)
|
||||
|
||||
if value is None:
|
||||
self._attr_available = False
|
||||
self._attr_is_on = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._attr_is_on = cast(bool, value)
|
||||
|
||||
self._attr_is_on = self.device.reachable
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -53,7 +53,6 @@ NETATMO_CREATE_ROOM_SENSOR = "netatmo_create_room_sensor"
|
||||
NETATMO_CREATE_SELECT = "netatmo_create_select"
|
||||
NETATMO_CREATE_SENSOR = "netatmo_create_sensor"
|
||||
NETATMO_CREATE_SWITCH = "netatmo_create_switch"
|
||||
NETATMO_CREATE_WEATHER_BINARY_SENSOR = "netatmo_create_weather_binary_sensor"
|
||||
NETATMO_CREATE_WEATHER_SENSOR = "netatmo_create_weather_sensor"
|
||||
|
||||
CONF_AREA_NAME = "area_name"
|
||||
|
||||
@@ -45,7 +45,6 @@ from .const import (
|
||||
NETATMO_CREATE_SELECT,
|
||||
NETATMO_CREATE_SENSOR,
|
||||
NETATMO_CREATE_SWITCH,
|
||||
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
|
||||
NETATMO_CREATE_WEATHER_SENSOR,
|
||||
PLATFORMS,
|
||||
WEBHOOK_ACTIVATION,
|
||||
@@ -333,20 +332,16 @@ class NetatmoDataHandler:
|
||||
"""Set up home coach/air care modules."""
|
||||
for module in self.account.modules.values():
|
||||
if module.device_category is NetatmoDeviceCategory.air_care:
|
||||
for signal in (
|
||||
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
NETATMO_CREATE_WEATHER_SENSOR,
|
||||
):
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
signal,
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
AIR_CARE,
|
||||
AIR_CARE,
|
||||
),
|
||||
)
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
AIR_CARE,
|
||||
AIR_CARE,
|
||||
),
|
||||
)
|
||||
|
||||
def setup_modules(self, home: pyatmo.Home, signal_home: str) -> None:
|
||||
"""Set up modules."""
|
||||
@@ -384,20 +379,16 @@ class NetatmoDataHandler:
|
||||
),
|
||||
)
|
||||
if module.device_category is NetatmoDeviceCategory.weather:
|
||||
for signal in (
|
||||
NETATMO_CREATE_WEATHER_BINARY_SENSOR,
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
NETATMO_CREATE_WEATHER_SENSOR,
|
||||
):
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
signal,
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
home.entity_id,
|
||||
WEATHER,
|
||||
),
|
||||
)
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
home.entity_id,
|
||||
WEATHER,
|
||||
),
|
||||
)
|
||||
|
||||
def setup_rooms(self, home: pyatmo.Home, signal_home: str) -> None:
|
||||
"""Set up rooms."""
|
||||
|
||||
@@ -55,7 +55,7 @@ SENSOR_TYPES: dict[str, OSOEnergySensorEntityDescription] = {
|
||||
key="optimization_mode",
|
||||
translation_key="optimization_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["off", "oso", "gridcompany", "smartcompany", "advanced", "nettleie"],
|
||||
options=["off", "oso", "gridcompany", "smartcompany", "advanced"],
|
||||
value_fn=lambda entity_data: entity_data.state.lower(),
|
||||
),
|
||||
"power_load": OSOEnergySensorEntityDescription(
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
"state": {
|
||||
"advanced": "Advanced",
|
||||
"gridcompany": "Grid company",
|
||||
"nettleie": "Nettleie",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"oso": "OSO",
|
||||
"smartcompany": "Smart company"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.7.1"]
|
||||
"requirements": ["python-otbr-api==2.7.0"]
|
||||
}
|
||||
|
||||
@@ -11,11 +11,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import (
|
||||
PowerfoxConfigEntry,
|
||||
PowerfoxDataUpdateCoordinator,
|
||||
PowerfoxReportDataUpdateCoordinator,
|
||||
)
|
||||
from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
@@ -34,16 +30,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerfoxConfigEntry) ->
|
||||
await client.close()
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
coordinators: list[
|
||||
PowerfoxDataUpdateCoordinator | PowerfoxReportDataUpdateCoordinator
|
||||
] = []
|
||||
for device in devices:
|
||||
if device.type == DeviceType.GAS_METER:
|
||||
coordinators.append(
|
||||
PowerfoxReportDataUpdateCoordinator(hass, entry, client, device)
|
||||
)
|
||||
continue
|
||||
coordinators.append(PowerfoxDataUpdateCoordinator(hass, entry, client, device))
|
||||
coordinators: list[PowerfoxDataUpdateCoordinator] = [
|
||||
PowerfoxDataUpdateCoordinator(hass, entry, client, device)
|
||||
for device in devices
|
||||
# Filter out gas meter devices (Powerfox FLOW adapters) as they are not yet supported and cause integration failures
|
||||
if device.type != DeviceType.GAS_METER
|
||||
]
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from powerfox import (
|
||||
Device,
|
||||
DeviceReport,
|
||||
Powerfox,
|
||||
PowerfoxAuthenticationError,
|
||||
PowerfoxConnectionError,
|
||||
@@ -18,18 +15,14 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
|
||||
type PowerfoxCoordinator = (
|
||||
"PowerfoxDataUpdateCoordinator" | "PowerfoxReportDataUpdateCoordinator"
|
||||
)
|
||||
type PowerfoxConfigEntry = ConfigEntry[list[PowerfoxCoordinator]]
|
||||
type PowerfoxConfigEntry = ConfigEntry[list[PowerfoxDataUpdateCoordinator]]
|
||||
|
||||
|
||||
class PowerfoxBaseCoordinator[T](DataUpdateCoordinator[T]):
|
||||
"""Base coordinator handling shared Powerfox logic."""
|
||||
class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[Poweropti]):
|
||||
"""Class to manage fetching Powerfox data from the API."""
|
||||
|
||||
config_entry: PowerfoxConfigEntry
|
||||
|
||||
@@ -40,7 +33,7 @@ class PowerfoxBaseCoordinator[T](DataUpdateCoordinator[T]):
|
||||
client: Powerfox,
|
||||
device: Device,
|
||||
) -> None:
|
||||
"""Initialize shared Powerfox coordinator."""
|
||||
"""Initialize global Powerfox data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
@@ -51,37 +44,11 @@ class PowerfoxBaseCoordinator[T](DataUpdateCoordinator[T]):
|
||||
self.client = client
|
||||
self.device = device
|
||||
|
||||
async def _async_update_data(self) -> T:
|
||||
"""Fetch data and normalize Powerfox errors."""
|
||||
async def _async_update_data(self) -> Poweropti:
|
||||
"""Fetch data from Powerfox API."""
|
||||
try:
|
||||
return await self._async_fetch_data()
|
||||
return await self.client.device(device_id=self.device.id)
|
||||
except PowerfoxAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
except (PowerfoxConnectionError, PowerfoxNoDataError) as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
||||
async def _async_fetch_data(self) -> T:
|
||||
"""Fetch data from the Powerfox API."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PowerfoxDataUpdateCoordinator(PowerfoxBaseCoordinator[Poweropti]):
|
||||
"""Class to manage fetching Powerfox data from the API."""
|
||||
|
||||
async def _async_fetch_data(self) -> Poweropti:
|
||||
"""Fetch live device data from the Powerfox API."""
|
||||
return await self.client.device(device_id=self.device.id)
|
||||
|
||||
|
||||
class PowerfoxReportDataUpdateCoordinator(PowerfoxBaseCoordinator[DeviceReport]):
|
||||
"""Coordinator handling report data from the API."""
|
||||
|
||||
async def _async_fetch_data(self) -> DeviceReport:
|
||||
"""Fetch report data from the Powerfox API."""
|
||||
local_now = datetime.now(tz=dt_util.get_time_zone(self.hass.config.time_zone))
|
||||
return await self.client.report(
|
||||
device_id=self.device.id,
|
||||
year=local_now.year,
|
||||
month=local_now.month,
|
||||
day=local_now.day,
|
||||
)
|
||||
|
||||
@@ -5,18 +5,18 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from powerfox import DeviceReport, HeatMeter, PowerMeter, WaterMeter
|
||||
from powerfox import HeatMeter, PowerMeter, WaterMeter
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import PowerfoxConfigEntry
|
||||
from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: PowerfoxConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for Powerfox config entry."""
|
||||
powerfox_data = entry.runtime_data
|
||||
powerfox_data: list[PowerfoxDataUpdateCoordinator] = entry.runtime_data
|
||||
|
||||
return {
|
||||
"devices": [
|
||||
@@ -68,21 +68,6 @@ async def async_get_config_entry_diagnostics(
|
||||
if isinstance(coordinator.data, HeatMeter)
|
||||
else {}
|
||||
),
|
||||
**(
|
||||
{
|
||||
"gas_meter": {
|
||||
"sum": coordinator.data.gas.sum,
|
||||
"consumption": coordinator.data.gas.consumption,
|
||||
"consumption_kwh": coordinator.data.gas.consumption_kwh,
|
||||
"current_consumption": coordinator.data.gas.current_consumption,
|
||||
"current_consumption_kwh": coordinator.data.gas.current_consumption_kwh,
|
||||
"sum_currency": coordinator.data.gas.sum_currency,
|
||||
}
|
||||
}
|
||||
if isinstance(coordinator.data, DeviceReport)
|
||||
and coordinator.data.gas
|
||||
else {}
|
||||
),
|
||||
}
|
||||
for coordinator in powerfox_data
|
||||
],
|
||||
|
||||
@@ -2,27 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from powerfox import Device
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PowerfoxBaseCoordinator
|
||||
from .coordinator import PowerfoxDataUpdateCoordinator
|
||||
|
||||
|
||||
class PowerfoxEntity[CoordinatorT: PowerfoxBaseCoordinator[Any]](
|
||||
CoordinatorEntity[CoordinatorT]
|
||||
):
|
||||
class PowerfoxEntity(CoordinatorEntity[PowerfoxDataUpdateCoordinator]):
|
||||
"""Base entity for Powerfox."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: CoordinatorT,
|
||||
coordinator: PowerfoxDataUpdateCoordinator,
|
||||
device: Device,
|
||||
) -> None:
|
||||
"""Initialize Powerfox entity."""
|
||||
|
||||
@@ -70,7 +70,10 @@ rules:
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not have any entities that should disabled by default.
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
|
||||
@@ -4,9 +4,8 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from powerfox import Device, GasReport, HeatMeter, PowerMeter, WaterMeter
|
||||
from powerfox import Device, HeatMeter, PowerMeter, WaterMeter
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -14,16 +13,11 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import CURRENCY_EURO, UnitOfEnergy, UnitOfPower, UnitOfVolume
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfPower, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import (
|
||||
PowerfoxBaseCoordinator,
|
||||
PowerfoxConfigEntry,
|
||||
PowerfoxDataUpdateCoordinator,
|
||||
PowerfoxReportDataUpdateCoordinator,
|
||||
)
|
||||
from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator
|
||||
from .entity import PowerfoxEntity
|
||||
|
||||
|
||||
@@ -36,13 +30,6 @@ class PowerfoxSensorEntityDescription[T: (PowerMeter, WaterMeter, HeatMeter)](
|
||||
value_fn: Callable[[T], float | int | None]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PowerfoxReportSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Powerfox report sensor entity."""
|
||||
|
||||
value_fn: Callable[[GasReport], float | int | None]
|
||||
|
||||
|
||||
SENSORS_POWER: tuple[PowerfoxSensorEntityDescription[PowerMeter], ...] = (
|
||||
PowerfoxSensorEntityDescription[PowerMeter](
|
||||
key="power",
|
||||
@@ -139,104 +126,6 @@ SENSORS_HEAT: tuple[PowerfoxSensorEntityDescription[HeatMeter], ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
SENSORS_GAS: tuple[PowerfoxReportSensorEntityDescription, ...] = (
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_consumption_today",
|
||||
translation_key="gas_consumption_today",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda gas: gas.sum,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_consumption_energy_today",
|
||||
translation_key="gas_consumption_energy_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.consumption_kwh,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_current_consumption",
|
||||
translation_key="gas_current_consumption",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
value_fn=lambda gas: gas.current_consumption,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_current_consumption_energy",
|
||||
translation_key="gas_current_consumption_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.current_consumption_kwh,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_cost_today",
|
||||
translation_key="gas_cost_today",
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
value_fn=lambda gas: gas.sum_currency,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_max_consumption_today",
|
||||
translation_key="gas_max_consumption_today",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
value_fn=lambda gas: gas.max_consumption,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_min_consumption_today",
|
||||
translation_key="gas_min_consumption_today",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
value_fn=lambda gas: gas.min_consumption,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_avg_consumption_today",
|
||||
translation_key="gas_avg_consumption_today",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.avg_consumption,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_max_consumption_energy_today",
|
||||
translation_key="gas_max_consumption_energy_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.max_consumption_kwh,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_min_consumption_energy_today",
|
||||
translation_key="gas_min_consumption_energy_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.min_consumption_kwh,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_avg_consumption_energy_today",
|
||||
translation_key="gas_avg_consumption_energy_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda gas: gas.avg_consumption_kwh,
|
||||
),
|
||||
PowerfoxReportSensorEntityDescription(
|
||||
key="gas_max_cost_today",
|
||||
translation_key="gas_max_cost_today",
|
||||
native_unit_of_measurement=CURRENCY_EURO,
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda gas: gas.max_currency,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -246,20 +135,6 @@ async def async_setup_entry(
|
||||
"""Set up Powerfox sensors based on a config entry."""
|
||||
entities: list[SensorEntity] = []
|
||||
for coordinator in entry.runtime_data:
|
||||
if isinstance(coordinator, PowerfoxReportDataUpdateCoordinator):
|
||||
gas_report = coordinator.data.gas
|
||||
if gas_report is None:
|
||||
continue
|
||||
entities.extend(
|
||||
PowerfoxGasSensorEntity(
|
||||
coordinator=coordinator,
|
||||
description=description,
|
||||
device=coordinator.device,
|
||||
)
|
||||
for description in SENSORS_GAS
|
||||
if description.value_fn(gas_report) is not None
|
||||
)
|
||||
continue
|
||||
if isinstance(coordinator.data, PowerMeter):
|
||||
entities.extend(
|
||||
PowerfoxSensorEntity(
|
||||
@@ -291,49 +166,23 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class BasePowerfoxSensorEntity[CoordinatorT: PowerfoxBaseCoordinator[Any]](
|
||||
PowerfoxEntity[CoordinatorT], SensorEntity
|
||||
):
|
||||
"""Common base for Powerfox sensor entities."""
|
||||
class PowerfoxSensorEntity(PowerfoxEntity, SensorEntity):
|
||||
"""Defines a powerfox power meter sensor."""
|
||||
|
||||
entity_description: SensorEntityDescription
|
||||
entity_description: PowerfoxSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: CoordinatorT,
|
||||
coordinator: PowerfoxDataUpdateCoordinator,
|
||||
device: Device,
|
||||
description: SensorEntityDescription,
|
||||
description: PowerfoxSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the shared Powerfox sensor."""
|
||||
"""Initialize Powerfox power meter sensor."""
|
||||
super().__init__(coordinator, device)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device.id}_{description.key}"
|
||||
|
||||
|
||||
class PowerfoxSensorEntity(BasePowerfoxSensorEntity[PowerfoxDataUpdateCoordinator]):
|
||||
"""Defines a powerfox poweropti sensor."""
|
||||
|
||||
coordinator: PowerfoxDataUpdateCoordinator
|
||||
entity_description: PowerfoxSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | int | None:
|
||||
"""Return the state of the entity."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
|
||||
class PowerfoxGasSensorEntity(
|
||||
BasePowerfoxSensorEntity[PowerfoxReportDataUpdateCoordinator]
|
||||
):
|
||||
"""Defines a powerfox gas meter sensor."""
|
||||
|
||||
coordinator: PowerfoxReportDataUpdateCoordinator
|
||||
entity_description: PowerfoxReportSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | int | None:
|
||||
"""Return the state of the entity."""
|
||||
gas_report = self.coordinator.data.gas
|
||||
if TYPE_CHECKING:
|
||||
assert gas_report is not None
|
||||
return self.entity_description.value_fn(gas_report)
|
||||
|
||||
@@ -62,42 +62,6 @@
|
||||
"energy_usage_low_tariff": {
|
||||
"name": "Energy usage low tariff"
|
||||
},
|
||||
"gas_avg_consumption_energy_today": {
|
||||
"name": "Avg gas hourly energy - today"
|
||||
},
|
||||
"gas_avg_consumption_today": {
|
||||
"name": "Avg gas hourly consumption - today"
|
||||
},
|
||||
"gas_consumption_energy_today": {
|
||||
"name": "Gas consumption energy - today"
|
||||
},
|
||||
"gas_consumption_today": {
|
||||
"name": "Gas consumption - today"
|
||||
},
|
||||
"gas_cost_today": {
|
||||
"name": "Gas cost - today"
|
||||
},
|
||||
"gas_current_consumption": {
|
||||
"name": "Gas consumption - this hour"
|
||||
},
|
||||
"gas_current_consumption_energy": {
|
||||
"name": "Gas consumption energy - this hour"
|
||||
},
|
||||
"gas_max_consumption_energy_today": {
|
||||
"name": "Max gas hourly energy - today"
|
||||
},
|
||||
"gas_max_consumption_today": {
|
||||
"name": "Max gas hourly consumption - today"
|
||||
},
|
||||
"gas_max_cost_today": {
|
||||
"name": "Max gas hourly cost - today"
|
||||
},
|
||||
"gas_min_consumption_energy_today": {
|
||||
"name": "Min gas hourly energy - today"
|
||||
},
|
||||
"gas_min_consumption_today": {
|
||||
"name": "Min gas hourly consumption - today"
|
||||
},
|
||||
"heat_delta_energy": {
|
||||
"name": "Delta energy"
|
||||
},
|
||||
|
||||
@@ -12,8 +12,6 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from .entity import TeslaFleetVehicleEntity
|
||||
from .models import TeslaFleetVehicleData
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
"data": {
|
||||
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"access_token": "[%key:component::tessie::config::step::user::data_description::access_token%]"
|
||||
},
|
||||
"description": "[%key:component::tessie::config::step::user::description%]",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
},
|
||||
@@ -24,9 +21,6 @@
|
||||
"data": {
|
||||
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"access_token": "Visit developer settings and select Generate Access Token."
|
||||
},
|
||||
"description": "Enter your access token from {url}."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.7.1", "pyroute2==0.7.5"],
|
||||
"requirements": ["python-otbr-api==2.7.0", "pyroute2==0.7.5"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_meshcop._udp.local."]
|
||||
}
|
||||
|
||||
@@ -74,8 +74,6 @@ class VeluxRainSensor(VeluxEntity, BinarySensorEntity):
|
||||
|
||||
self._attr_available = True
|
||||
|
||||
# Velux windows with rain sensors report an opening limitation when rain is detected.
|
||||
# So far we've seen 89, 91, 93 (most cases) or 100 (Velux GPU). It probably makes sense to
|
||||
# assume that any large enough limitation (we use >=89) means rain is detected.
|
||||
# Documentation on this is non-existent AFAIK.
|
||||
self._attr_is_on = limitation.min_value >= 89
|
||||
# Velux windows with rain sensors report an opening limitation of 93 or 100 (Velux GPU) when rain is detected.
|
||||
# So far, only 93 and 100 have been observed in practice, documentation on this is non-existent AFAIK.
|
||||
self._attr_is_on = limitation.min_value in {93, 100}
|
||||
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -278,7 +278,6 @@ FLOWS = {
|
||||
"habitica",
|
||||
"hanna",
|
||||
"harmony",
|
||||
"hdfury",
|
||||
"heos",
|
||||
"here_travel_time",
|
||||
"hikvision",
|
||||
|
||||
@@ -2678,12 +2678,6 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"hdfury": {
|
||||
"name": "HDFury",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"hdmi_cec": {
|
||||
"name": "HDMI-CEC",
|
||||
"integration_type": "hub",
|
||||
|
||||
5
requirements_all.txt
generated
5
requirements_all.txt
generated
@@ -1184,9 +1184,6 @@ hassil==3.5.0
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate[astral]==1.1.2
|
||||
|
||||
# homeassistant.components.hdfury
|
||||
hdfury==1.3.1
|
||||
|
||||
# homeassistant.components.heatmiser
|
||||
heatmiserV3==2.0.4
|
||||
|
||||
@@ -2566,7 +2563,7 @@ python-opensky==1.0.1
|
||||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==2.7.1
|
||||
python-otbr-api==2.7.0
|
||||
|
||||
# homeassistant.components.overseerr
|
||||
python-overseerr==0.8.0
|
||||
|
||||
5
requirements_test_all.txt
generated
5
requirements_test_all.txt
generated
@@ -1051,9 +1051,6 @@ hassil==3.5.0
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate[astral]==1.1.2
|
||||
|
||||
# homeassistant.components.hdfury
|
||||
hdfury==1.3.1
|
||||
|
||||
# homeassistant.components.here_travel_time
|
||||
here-routing==1.2.0
|
||||
|
||||
@@ -2156,7 +2153,7 @@ python-opensky==1.0.1
|
||||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==2.7.1
|
||||
python-otbr-api==2.7.0
|
||||
|
||||
# homeassistant.components.overseerr
|
||||
python-overseerr==0.8.0
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Tests for the HDFury integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Fixture for setting up the integration."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -1,79 +0,0 @@
|
||||
"""Common fixtures for the HDFury tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hdfury.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_HOST = "192.168.1.123"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.hdfury.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="000123456789",
|
||||
data={
|
||||
CONF_HOST: TEST_HOST,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_hdfury_client() -> Generator[AsyncMock]:
|
||||
"""Mock a HDFury client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hdfury.config_flow.HDFuryAPI",
|
||||
autospec=True,
|
||||
) as mock_cf_client,
|
||||
patch(
|
||||
"homeassistant.components.hdfury.coordinator.HDFuryAPI",
|
||||
autospec=True,
|
||||
) as mock_coord_client,
|
||||
):
|
||||
# Config flow client
|
||||
cf_client = mock_cf_client.return_value
|
||||
cf_client.get_board = AsyncMock(
|
||||
return_value={
|
||||
"hostname": "VRROOM-02",
|
||||
"ipaddress": "192.168.1.123",
|
||||
"serial": "000123456789",
|
||||
"pcbv": "3",
|
||||
"version": "FW: 0.61",
|
||||
}
|
||||
)
|
||||
|
||||
# Coordinator client
|
||||
coord_client = mock_coord_client.return_value
|
||||
coord_client.get_board = cf_client.get_board
|
||||
coord_client.get_info = AsyncMock(
|
||||
return_value={
|
||||
"portseltx0": "0",
|
||||
"portseltx1": "4",
|
||||
"opmode": "0",
|
||||
}
|
||||
)
|
||||
coord_client.get_config = AsyncMock(
|
||||
return_value={
|
||||
"macaddr": "c7:1c:df:9d:f6:40",
|
||||
}
|
||||
)
|
||||
|
||||
yield coord_client
|
||||
@@ -1,192 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_select_entities[select.hdfury_vrroom_02_operation_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.hdfury_vrroom_02_operation_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Operation mode',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'opmode',
|
||||
'unique_id': '000123456789_opmode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_select_entities[select.hdfury_vrroom_02_operation_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 Operation mode',
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.hdfury_vrroom_02_operation_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_select_entities[select.hdfury_vrroom_02_port_select_tx0-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.hdfury_vrroom_02_port_select_tx0',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Port select TX0',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'portseltx0',
|
||||
'unique_id': '000123456789_portseltx0',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_select_entities[select.hdfury_vrroom_02_port_select_tx0-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 Port select TX0',
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.hdfury_vrroom_02_port_select_tx0',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_select_entities[select.hdfury_vrroom_02_port_select_tx1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.hdfury_vrroom_02_port_select_tx1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Port select TX1',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'portseltx1',
|
||||
'unique_id': '000123456789_portseltx1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_select_entities[select.hdfury_vrroom_02_port_select_tx1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 Port select TX1',
|
||||
'options': list([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.hdfury_vrroom_02_port_select_tx1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '4',
|
||||
})
|
||||
# ---
|
||||
@@ -1,96 +0,0 @@
|
||||
"""Test the HDFury config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from hdfury import HDFuryError
|
||||
|
||||
from homeassistant.components.hdfury.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_async_step_user_gets_form_and_creates_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_hdfury_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that the we can view the form and that the config flow creates an entry."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "192.168.1.123"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "192.168.1.123",
|
||||
}
|
||||
assert result["result"].unique_id == "000123456789"
|
||||
|
||||
|
||||
async def test_abort_if_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that we abort if we attempt to submit the same entry twice."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "192.168.1.123"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_successful_recovery_after_connection_error(
|
||||
hass: HomeAssistant,
|
||||
mock_hdfury_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test error shown when connection fails."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
# Simulate a connection error by raising a HDFuryError
|
||||
mock_hdfury_client.get_board.side_effect = HDFuryError()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "192.168.1.123"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
# Simulate successful connection on retry
|
||||
mock_hdfury_client.get_board.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "192.168.1.123"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "192.168.1.123",
|
||||
}
|
||||
assert result["result"].unique_id == "000123456789"
|
||||
@@ -1,30 +0,0 @@
|
||||
"""Tests for the HDFury select platform."""
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.SELECT]
|
||||
|
||||
|
||||
async def test_select_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test HDFury select entities."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
@@ -4,15 +4,7 @@ from collections.abc import Generator
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from powerfox import (
|
||||
Device,
|
||||
DeviceReport,
|
||||
DeviceType,
|
||||
GasReport,
|
||||
HeatMeter,
|
||||
PowerMeter,
|
||||
WaterMeter,
|
||||
)
|
||||
from powerfox import Device, DeviceType, HeatMeter, PowerMeter, WaterMeter
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.powerfox.const import DOMAIN
|
||||
@@ -21,64 +13,6 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
def _power_meter() -> PowerMeter:
|
||||
"""Return a mocked power meter reading."""
|
||||
return PowerMeter(
|
||||
outdated=False,
|
||||
timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC),
|
||||
power=111,
|
||||
energy_usage=1111.111,
|
||||
energy_return=111.111,
|
||||
energy_usage_high_tariff=111.111,
|
||||
energy_usage_low_tariff=111.111,
|
||||
)
|
||||
|
||||
|
||||
def _water_meter() -> WaterMeter:
|
||||
"""Return a mocked water meter reading."""
|
||||
return WaterMeter(
|
||||
outdated=False,
|
||||
timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC),
|
||||
cold_water=1111.111,
|
||||
warm_water=0.0,
|
||||
)
|
||||
|
||||
|
||||
def _heat_meter() -> HeatMeter:
|
||||
"""Return a mocked heat meter reading."""
|
||||
return HeatMeter(
|
||||
outdated=False,
|
||||
timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC),
|
||||
total_energy=1111.111,
|
||||
delta_energy=111,
|
||||
total_volume=1111.111,
|
||||
delta_volume=0.111,
|
||||
)
|
||||
|
||||
|
||||
def _gas_report() -> DeviceReport:
|
||||
"""Return a mocked gas report."""
|
||||
return DeviceReport(
|
||||
gas=GasReport(
|
||||
total_delta=100,
|
||||
sum=10.5,
|
||||
total_delta_currency=5.0,
|
||||
current_consumption=0.4,
|
||||
current_consumption_kwh=4.0,
|
||||
consumption=10.5,
|
||||
consumption_kwh=10.5,
|
||||
max_consumption=1.5,
|
||||
min_consumption=0.2,
|
||||
avg_consumption=0.6,
|
||||
max_consumption_kwh=1.7,
|
||||
min_consumption_kwh=0.1,
|
||||
avg_consumption_kwh=0.5,
|
||||
sum_currency=2.5,
|
||||
max_currency=0.3,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
@@ -127,25 +61,32 @@ def mock_powerfox_client() -> Generator[AsyncMock]:
|
||||
type=DeviceType.HEAT_METER,
|
||||
name="Heatopti",
|
||||
),
|
||||
Device(
|
||||
id="9x9x1f12xx6x",
|
||||
date_added=datetime(2024, 11, 26, 9, 22, 35, tzinfo=UTC),
|
||||
main_device=False,
|
||||
bidirectional=False,
|
||||
type=DeviceType.GAS_METER,
|
||||
name="Gasopti",
|
||||
]
|
||||
client.device.side_effect = [
|
||||
PowerMeter(
|
||||
outdated=False,
|
||||
timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC),
|
||||
power=111,
|
||||
energy_usage=1111.111,
|
||||
energy_return=111.111,
|
||||
energy_usage_high_tariff=111.111,
|
||||
energy_usage_low_tariff=111.111,
|
||||
),
|
||||
WaterMeter(
|
||||
outdated=False,
|
||||
timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC),
|
||||
cold_water=1111.111,
|
||||
warm_water=0.0,
|
||||
),
|
||||
HeatMeter(
|
||||
outdated=False,
|
||||
timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC),
|
||||
total_energy=1111.111,
|
||||
delta_energy=111,
|
||||
total_volume=1111.111,
|
||||
delta_volume=0.111,
|
||||
),
|
||||
]
|
||||
device_factories = {
|
||||
"9x9x1f12xx3x": _power_meter,
|
||||
"9x9x1f12xx4x": _water_meter,
|
||||
"9x9x1f12xx5x": _heat_meter,
|
||||
}
|
||||
|
||||
client.device = AsyncMock(
|
||||
side_effect=lambda *, device_id: device_factories[device_id]() # type: ignore[index]
|
||||
)
|
||||
client.report = AsyncMock(return_value=_gas_report())
|
||||
yield client
|
||||
|
||||
|
||||
|
||||
@@ -31,16 +31,6 @@
|
||||
'total_volume': 1111.111,
|
||||
}),
|
||||
}),
|
||||
dict({
|
||||
'gas_meter': dict({
|
||||
'consumption': 10.5,
|
||||
'consumption_kwh': 10.5,
|
||||
'current_consumption': 0.4,
|
||||
'current_consumption_kwh': 4.0,
|
||||
'sum': 10.5,
|
||||
'sum_currency': 2.5,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -1,649 +1,4 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_sensors[sensor.gasopti_avg_gas_hourly_consumption_today-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_avg_gas_hourly_consumption_today',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.GAS: 'gas'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Avg gas hourly consumption - today',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_avg_consumption_today',
|
||||
'unique_id': '9x9x1f12xx6x_gas_avg_consumption_today',
|
||||
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_avg_gas_hourly_consumption_today-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'gas',
|
||||
'friendly_name': 'Gasopti Avg gas hourly consumption - today',
|
||||
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_avg_gas_hourly_consumption_today',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.6',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_avg_gas_hourly_energy_today-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_avg_gas_hourly_energy_today',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Avg gas hourly energy - today',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_avg_consumption_energy_today',
|
||||
'unique_id': '9x9x1f12xx6x_gas_avg_consumption_energy_today',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_avg_gas_hourly_energy_today-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Gasopti Avg gas hourly energy - today',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_avg_gas_hourly_energy_today',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_gas_consumption_energy_this_hour-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_gas_consumption_energy_this_hour',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Gas consumption energy - this hour',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_current_consumption_energy',
|
||||
'unique_id': '9x9x1f12xx6x_gas_current_consumption_energy',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_gas_consumption_energy_this_hour-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Gasopti Gas consumption energy - this hour',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_gas_consumption_energy_this_hour',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '4.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_gas_consumption_energy_today-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_gas_consumption_energy_today',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Gas consumption energy - today',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_consumption_energy_today',
|
||||
'unique_id': '9x9x1f12xx6x_gas_consumption_energy_today',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_gas_consumption_energy_today-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Gasopti Gas consumption energy - today',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_gas_consumption_energy_today',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '10.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_gas_consumption_this_hour-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_gas_consumption_this_hour',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.GAS: 'gas'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Gas consumption - this hour',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_current_consumption',
|
||||
'unique_id': '9x9x1f12xx6x_gas_current_consumption',
|
||||
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_gas_consumption_this_hour-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'gas',
|
||||
'friendly_name': 'Gasopti Gas consumption - this hour',
|
||||
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_gas_consumption_this_hour',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.4',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_gas_consumption_today-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_gas_consumption_today',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.GAS: 'gas'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Gas consumption - today',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_consumption_today',
|
||||
'unique_id': '9x9x1f12xx6x_gas_consumption_today',
|
||||
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_gas_consumption_today-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'gas',
|
||||
'friendly_name': 'Gasopti Gas consumption - today',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_gas_consumption_today',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '10.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_gas_cost_today-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_gas_cost_today',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Gas cost - today',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_cost_today',
|
||||
'unique_id': '9x9x1f12xx6x_gas_cost_today',
|
||||
'unit_of_measurement': '€',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_gas_cost_today-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'monetary',
|
||||
'friendly_name': 'Gasopti Gas cost - today',
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': '€',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_gas_cost_today',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_max_gas_hourly_consumption_today-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_max_gas_hourly_consumption_today',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.GAS: 'gas'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Max gas hourly consumption - today',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_max_consumption_today',
|
||||
'unique_id': '9x9x1f12xx6x_gas_max_consumption_today',
|
||||
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_max_gas_hourly_consumption_today-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'gas',
|
||||
'friendly_name': 'Gasopti Max gas hourly consumption - today',
|
||||
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_max_gas_hourly_consumption_today',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_max_gas_hourly_cost_today-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_max_gas_hourly_cost_today',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Max gas hourly cost - today',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_max_cost_today',
|
||||
'unique_id': '9x9x1f12xx6x_gas_max_cost_today',
|
||||
'unit_of_measurement': '€',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_max_gas_hourly_cost_today-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'monetary',
|
||||
'friendly_name': 'Gasopti Max gas hourly cost - today',
|
||||
'unit_of_measurement': '€',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_max_gas_hourly_cost_today',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.3',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_max_gas_hourly_energy_today-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_max_gas_hourly_energy_today',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Max gas hourly energy - today',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_max_consumption_energy_today',
|
||||
'unique_id': '9x9x1f12xx6x_gas_max_consumption_energy_today',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_max_gas_hourly_energy_today-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Gasopti Max gas hourly energy - today',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_max_gas_hourly_energy_today',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1.7',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_min_gas_hourly_consumption_today-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_min_gas_hourly_consumption_today',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.GAS: 'gas'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Min gas hourly consumption - today',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_min_consumption_today',
|
||||
'unique_id': '9x9x1f12xx6x_gas_min_consumption_today',
|
||||
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_min_gas_hourly_consumption_today-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'gas',
|
||||
'friendly_name': 'Gasopti Min gas hourly consumption - today',
|
||||
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_min_gas_hourly_consumption_today',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_min_gas_hourly_energy_today-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.gasopti_min_gas_hourly_energy_today',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Min gas hourly energy - today',
|
||||
'platform': 'powerfox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'gas_min_consumption_energy_today',
|
||||
'unique_id': '9x9x1f12xx6x_gas_min_consumption_energy_today',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.gasopti_min_gas_hourly_energy_today-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Gasopti Min gas hourly energy - today',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gasopti_min_gas_hourly_energy_today',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.1',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensors[sensor.heatopti_delta_energy-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -6,8 +6,7 @@ from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from powerfox import DeviceReport, PowerfoxConnectionError
|
||||
import pytest
|
||||
from powerfox import PowerfoxConnectionError
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@@ -20,7 +19,6 @@ from . import setup_integration
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_all_sensors(
|
||||
hass: HomeAssistant,
|
||||
mock_powerfox_client: AsyncMock,
|
||||
@@ -53,17 +51,3 @@ async def test_update_failed(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.poweropti_energy_usage").state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_skips_gas_sensors_when_report_missing(
|
||||
hass: HomeAssistant,
|
||||
mock_powerfox_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test gas sensors are not created when report lacks gas data."""
|
||||
mock_powerfox_client.report.return_value = DeviceReport(gas=None)
|
||||
|
||||
with patch("homeassistant.components.powerfox.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert hass.states.get("sensor.gasopti_gas_consumption_today") is None
|
||||
|
||||
@@ -49,29 +49,15 @@ async def test_rain_sensor_state(
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# simulate rain detected (most Velux models report 93)
|
||||
# simulate rain detected (other Velux models report 93)
|
||||
mock_window.get_limitation.return_value.min_value = 93
|
||||
await update_polled_entities(hass, freezer)
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# simulate rain detected (other Velux models report 89)
|
||||
mock_window.get_limitation.return_value.min_value = 89
|
||||
await update_polled_entities(hass, freezer)
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# simulate other limits which do not indicate rain detected
|
||||
mock_window.get_limitation.return_value.min_value = 88
|
||||
await update_polled_entities(hass, freezer)
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# simulate no rain detected again
|
||||
mock_window.get_limitation.return_value.min_value = 0
|
||||
mock_window.get_limitation.return_value.min_value = 95
|
||||
await update_polled_entities(hass, freezer)
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state is not None
|
||||
@@ -158,7 +144,7 @@ async def test_rain_sensor_unavailability(
|
||||
|
||||
# Simulate recovery
|
||||
mock_window.get_limitation.side_effect = None
|
||||
mock_window.get_limitation.return_value.min_value = 0
|
||||
mock_window.get_limitation.return_value.min_value = 95
|
||||
await update_polled_entities(hass, freezer)
|
||||
|
||||
# Entity should be available again
|
||||
|
||||
Reference in New Issue
Block a user