mirror of
https://github.com/home-assistant/core.git
synced 2026-01-16 20:46:56 +01:00
Compare commits
29 Commits
add_switch
...
homevolt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5aa32491c8 | ||
|
|
1293e7ed70 | ||
|
|
3e81cea99f | ||
|
|
4ce2dae701 | ||
|
|
a14a8c4e43 | ||
|
|
89e734d2de | ||
|
|
26c81f29e9 | ||
|
|
ce82e88919 | ||
|
|
60316a1232 | ||
|
|
aca4d3c5e6 | ||
|
|
9a93096e4b | ||
|
|
dc2cd2246b | ||
|
|
181037820b | ||
|
|
6cf15bf70c | ||
|
|
5a34c31e42 | ||
|
|
9dcc86f12e | ||
|
|
04429a6eef | ||
|
|
51e2506afb | ||
|
|
e49e5c7c40 | ||
|
|
b8dfc523da | ||
|
|
a25fbf57ef | ||
|
|
dac22002b0 | ||
|
|
e61f00a3ae | ||
|
|
14a67c6b5d | ||
|
|
90ae81f02b | ||
|
|
a741f214da | ||
|
|
21d0bd3ce2 | ||
|
|
d9c1f4850a | ||
|
|
335994af7e |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -711,6 +711,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/homematic/ @pvizeli
|
||||
/homeassistant/components/homematicip_cloud/ @hahn-th
|
||||
/tests/components/homematicip_cloud/ @hahn-th
|
||||
/homeassistant/components/homevolt/ @danielhiversen
|
||||
/tests/components/homevolt/ @danielhiversen
|
||||
/homeassistant/components/homewizard/ @DCSBL
|
||||
/tests/components/homewizard/ @DCSBL
|
||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||
|
||||
@@ -14,7 +14,7 @@ from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelStat
|
||||
|
||||
|
||||
def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
|
||||
"""Get the device class of an entity or UNDEFINED if not found."""
|
||||
"""Test if an entity supports the specified features."""
|
||||
try:
|
||||
return bool(get_supported_features(hass, entity_id) & features)
|
||||
except HomeAssistantError:
|
||||
@@ -39,7 +39,7 @@ class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
|
||||
def make_entity_state_trigger_required_features(
|
||||
domain: str, to_state: str, required_features: int
|
||||
) -> type[EntityTargetStateTriggerBase]:
|
||||
"""Create an entity state trigger class."""
|
||||
"""Create an entity state trigger class with required feature filtering."""
|
||||
|
||||
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
@@ -125,7 +125,6 @@ NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
||||
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"fan",
|
||||
"light",
|
||||
"switch",
|
||||
}
|
||||
|
||||
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""BleBox sensor entities."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import blebox_uniapi.sensor
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -146,7 +148,7 @@ class BleBoxSensorEntity(BleBoxEntity[blebox_uniapi.sensor.BaseSensor], SensorEn
|
||||
return self._feature.native_value
|
||||
|
||||
@property
|
||||
def last_reset(self):
|
||||
def last_reset(self) -> datetime | None:
|
||||
"""Return the time when the sensor was last reset, if implemented."""
|
||||
native_implementation = getattr(self._feature, "last_reset", None)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for Ebusd daemon for communication with eBUS heating systems."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import ebusdpy
|
||||
import voluptuous as vol
|
||||
@@ -17,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, SENSOR_TYPES
|
||||
from .const import DOMAIN, EBUSD_DATA, SENSOR_TYPES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,9 +29,9 @@ CACHE_TTL = 900
|
||||
SERVICE_EBUSD_WRITE = "ebusd_write"
|
||||
|
||||
|
||||
def verify_ebusd_config(config):
|
||||
def verify_ebusd_config(config: ConfigType) -> ConfigType:
|
||||
"""Verify eBusd config."""
|
||||
circuit = config[CONF_CIRCUIT]
|
||||
circuit: str = config[CONF_CIRCUIT]
|
||||
for condition in config[CONF_MONITORED_CONDITIONS]:
|
||||
if condition not in SENSOR_TYPES[circuit]:
|
||||
raise vol.Invalid(f"Condition '{condition}' not in '{circuit}'.")
|
||||
@@ -59,17 +60,17 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the eBusd component."""
|
||||
_LOGGER.debug("Integration setup started")
|
||||
conf = config[DOMAIN]
|
||||
name = conf[CONF_NAME]
|
||||
circuit = conf[CONF_CIRCUIT]
|
||||
monitored_conditions = conf.get(CONF_MONITORED_CONDITIONS)
|
||||
server_address = (conf.get(CONF_HOST), conf.get(CONF_PORT))
|
||||
conf: ConfigType = config[DOMAIN]
|
||||
name: str = conf[CONF_NAME]
|
||||
circuit: str = conf[CONF_CIRCUIT]
|
||||
monitored_conditions: list[str] = conf[CONF_MONITORED_CONDITIONS]
|
||||
server_address: tuple[str, int] = (conf[CONF_HOST], conf[CONF_PORT])
|
||||
|
||||
try:
|
||||
ebusdpy.init(server_address)
|
||||
except (TimeoutError, OSError):
|
||||
return False
|
||||
hass.data[DOMAIN] = EbusdData(server_address, circuit)
|
||||
hass.data[EBUSD_DATA] = EbusdData(server_address, circuit)
|
||||
sensor_config = {
|
||||
CONF_MONITORED_CONDITIONS: monitored_conditions,
|
||||
"client_name": name,
|
||||
@@ -77,7 +78,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
}
|
||||
load_platform(hass, Platform.SENSOR, DOMAIN, sensor_config, config)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write)
|
||||
hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[EBUSD_DATA].write)
|
||||
|
||||
_LOGGER.debug("Ebusd integration setup completed")
|
||||
return True
|
||||
@@ -86,13 +87,13 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
class EbusdData:
|
||||
"""Get the latest data from Ebusd."""
|
||||
|
||||
def __init__(self, address, circuit):
|
||||
def __init__(self, address: tuple[str, int], circuit: str) -> None:
|
||||
"""Initialize the data object."""
|
||||
self._circuit = circuit
|
||||
self._address = address
|
||||
self.value = {}
|
||||
self.value: dict[str, Any] = {}
|
||||
|
||||
def update(self, name, stype):
|
||||
def update(self, name: str, stype: int) -> None:
|
||||
"""Call the Ebusd API to update the data."""
|
||||
try:
|
||||
_LOGGER.debug("Opening socket to ebusd %s", name)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""Constants for ebus component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
@@ -8,277 +12,283 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import EbusdData
|
||||
|
||||
DOMAIN = "ebusd"
|
||||
EBUSD_DATA: HassKey[EbusdData] = HassKey(DOMAIN)
|
||||
|
||||
# SensorTypes from ebusdpy module :
|
||||
# 0='decimal', 1='time-schedule', 2='switch', 3='string', 4='value;status'
|
||||
|
||||
SENSOR_TYPES = {
|
||||
type SensorSpecs = tuple[str, str | None, str | None, int, SensorDeviceClass | None]
|
||||
SENSOR_TYPES: dict[str, dict[str, SensorSpecs]] = {
|
||||
"700": {
|
||||
"ActualFlowTemperatureDesired": [
|
||||
"ActualFlowTemperatureDesired": (
|
||||
"Hc1ActualFlowTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"MaxFlowTemperatureDesired": [
|
||||
),
|
||||
"MaxFlowTemperatureDesired": (
|
||||
"Hc1MaxFlowTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"MinFlowTemperatureDesired": [
|
||||
),
|
||||
"MinFlowTemperatureDesired": (
|
||||
"Hc1MinFlowTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"PumpStatus": ["Hc1PumpStatus", None, "mdi:toggle-switch", 2, None],
|
||||
"HCSummerTemperatureLimit": [
|
||||
),
|
||||
"PumpStatus": ("Hc1PumpStatus", None, "mdi:toggle-switch", 2, None),
|
||||
"HCSummerTemperatureLimit": (
|
||||
"Hc1SummerTempLimit",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
"mdi:weather-sunny",
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"HolidayTemperature": [
|
||||
),
|
||||
"HolidayTemperature": (
|
||||
"HolidayTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"HWTemperatureDesired": [
|
||||
),
|
||||
"HWTemperatureDesired": (
|
||||
"HwcTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"HWActualTemperature": [
|
||||
),
|
||||
"HWActualTemperature": (
|
||||
"HwcStorageTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerThursday": ["hwcTimer.Thursday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer-outline", 1, None],
|
||||
"HWOperativeMode": ["HwcOpMode", None, "mdi:math-compass", 3, None],
|
||||
"WaterPressure": [
|
||||
),
|
||||
"HWTimerMonday": ("hwcTimer.Monday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerTuesday": ("hwcTimer.Tuesday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerWednesday": ("hwcTimer.Wednesday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerThursday": ("hwcTimer.Thursday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerFriday": ("hwcTimer.Friday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerSaturday": ("hwcTimer.Saturday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerSunday": ("hwcTimer.Sunday", None, "mdi:timer-outline", 1, None),
|
||||
"HWOperativeMode": ("HwcOpMode", None, "mdi:math-compass", 3, None),
|
||||
"WaterPressure": (
|
||||
"WaterPressure",
|
||||
UnitOfPressure.BAR,
|
||||
"mdi:water-pump",
|
||||
0,
|
||||
SensorDeviceClass.PRESSURE,
|
||||
],
|
||||
"Zone1RoomZoneMapping": ["z1RoomZoneMapping", None, "mdi:label", 0, None],
|
||||
"Zone1NightTemperature": [
|
||||
),
|
||||
"Zone1RoomZoneMapping": ("z1RoomZoneMapping", None, "mdi:label", 0, None),
|
||||
"Zone1NightTemperature": (
|
||||
"z1NightTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
"mdi:weather-night",
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Zone1DayTemperature": [
|
||||
),
|
||||
"Zone1DayTemperature": (
|
||||
"z1DayTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
"mdi:weather-sunny",
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Zone1HolidayTemperature": [
|
||||
),
|
||||
"Zone1HolidayTemperature": (
|
||||
"z1HolidayTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Zone1RoomTemperature": [
|
||||
),
|
||||
"Zone1RoomTemperature": (
|
||||
"z1RoomTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Zone1ActualRoomTemperatureDesired": [
|
||||
),
|
||||
"Zone1ActualRoomTemperatureDesired": (
|
||||
"z1ActualRoomTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Zone1TimerMonday": ["z1Timer.Monday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1TimerTuesday": ["z1Timer.Tuesday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1TimerWednesday": [
|
||||
),
|
||||
"Zone1TimerMonday": ("z1Timer.Monday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1TimerTuesday": ("z1Timer.Tuesday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1TimerWednesday": (
|
||||
"z1Timer.Wednesday",
|
||||
None,
|
||||
"mdi:timer-outline",
|
||||
1,
|
||||
None,
|
||||
],
|
||||
"Zone1TimerThursday": ["z1Timer.Thursday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1TimerFriday": ["z1Timer.Friday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1OperativeMode": ["z1OpMode", None, "mdi:math-compass", 3, None],
|
||||
"ContinuosHeating": [
|
||||
),
|
||||
"Zone1TimerThursday": ("z1Timer.Thursday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1TimerFriday": ("z1Timer.Friday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1TimerSaturday": ("z1Timer.Saturday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1TimerSunday": ("z1Timer.Sunday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1OperativeMode": ("z1OpMode", None, "mdi:math-compass", 3, None),
|
||||
"ContinuosHeating": (
|
||||
"ContinuosHeating",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
"mdi:weather-snowy",
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"PowerEnergyConsumptionLastMonth": [
|
||||
),
|
||||
"PowerEnergyConsumptionLastMonth": (
|
||||
"PrEnergySumHcLastMonth",
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
"mdi:flash",
|
||||
0,
|
||||
SensorDeviceClass.ENERGY,
|
||||
],
|
||||
"PowerEnergyConsumptionThisMonth": [
|
||||
),
|
||||
"PowerEnergyConsumptionThisMonth": (
|
||||
"PrEnergySumHcThisMonth",
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
"mdi:flash",
|
||||
0,
|
||||
SensorDeviceClass.ENERGY,
|
||||
],
|
||||
),
|
||||
},
|
||||
"ehp": {
|
||||
"HWTemperature": [
|
||||
"HWTemperature": (
|
||||
"HwcTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"OutsideTemp": [
|
||||
),
|
||||
"OutsideTemp": (
|
||||
"OutsideTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
),
|
||||
},
|
||||
"bai": {
|
||||
"HotWaterTemperature": [
|
||||
"HotWaterTemperature": (
|
||||
"HwcTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"StorageTemperature": [
|
||||
),
|
||||
"StorageTemperature": (
|
||||
"StorageTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"DesiredStorageTemperature": [
|
||||
),
|
||||
"DesiredStorageTemperature": (
|
||||
"StorageTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"OutdoorsTemperature": [
|
||||
),
|
||||
"OutdoorsTemperature": (
|
||||
"OutdoorstempSensor",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"WaterPressure": [
|
||||
),
|
||||
"WaterPressure": (
|
||||
"WaterPressure",
|
||||
UnitOfPressure.BAR,
|
||||
"mdi:pipe",
|
||||
4,
|
||||
SensorDeviceClass.PRESSURE,
|
||||
],
|
||||
"AverageIgnitionTime": [
|
||||
),
|
||||
"AverageIgnitionTime": (
|
||||
"averageIgnitiontime",
|
||||
UnitOfTime.SECONDS,
|
||||
"mdi:av-timer",
|
||||
0,
|
||||
SensorDeviceClass.DURATION,
|
||||
],
|
||||
"MaximumIgnitionTime": [
|
||||
),
|
||||
"MaximumIgnitionTime": (
|
||||
"maxIgnitiontime",
|
||||
UnitOfTime.SECONDS,
|
||||
"mdi:av-timer",
|
||||
0,
|
||||
SensorDeviceClass.DURATION,
|
||||
],
|
||||
"MinimumIgnitionTime": [
|
||||
),
|
||||
"MinimumIgnitionTime": (
|
||||
"minIgnitiontime",
|
||||
UnitOfTime.SECONDS,
|
||||
"mdi:av-timer",
|
||||
0,
|
||||
SensorDeviceClass.DURATION,
|
||||
],
|
||||
"ReturnTemperature": [
|
||||
),
|
||||
"ReturnTemperature": (
|
||||
"ReturnTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"CentralHeatingPump": ["WP", None, "mdi:toggle-switch", 2, None],
|
||||
"HeatingSwitch": ["HeatingSwitch", None, "mdi:toggle-switch", 2, None],
|
||||
"DesiredFlowTemperature": [
|
||||
),
|
||||
"CentralHeatingPump": ("WP", None, "mdi:toggle-switch", 2, None),
|
||||
"HeatingSwitch": ("HeatingSwitch", None, "mdi:toggle-switch", 2, None),
|
||||
"DesiredFlowTemperature": (
|
||||
"FlowTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"FlowTemperature": [
|
||||
),
|
||||
"FlowTemperature": (
|
||||
"FlowTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Flame": ["Flame", None, "mdi:toggle-switch", 2, None],
|
||||
"PowerEnergyConsumptionHeatingCircuit": [
|
||||
),
|
||||
"Flame": ("Flame", None, "mdi:toggle-switch", 2, None),
|
||||
"PowerEnergyConsumptionHeatingCircuit": (
|
||||
"PrEnergySumHc1",
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
"mdi:flash",
|
||||
0,
|
||||
SensorDeviceClass.ENERGY,
|
||||
],
|
||||
"PowerEnergyConsumptionHotWaterCircuit": [
|
||||
),
|
||||
"PowerEnergyConsumptionHotWaterCircuit": (
|
||||
"PrEnergySumHwc1",
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
"mdi:flash",
|
||||
0,
|
||||
SensorDeviceClass.ENERGY,
|
||||
],
|
||||
"RoomThermostat": ["DCRoomthermostat", None, "mdi:toggle-switch", 2, None],
|
||||
"HeatingPartLoad": [
|
||||
),
|
||||
"RoomThermostat": ("DCRoomthermostat", None, "mdi:toggle-switch", 2, None),
|
||||
"HeatingPartLoad": (
|
||||
"PartloadHcKW",
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
"mdi:flash",
|
||||
0,
|
||||
SensorDeviceClass.ENERGY,
|
||||
],
|
||||
"StateNumber": ["StateNumber", None, "mdi:fire", 3, None],
|
||||
"ModulationPercentage": [
|
||||
),
|
||||
"StateNumber": ("StateNumber", None, "mdi:fire", 3, None),
|
||||
"ModulationPercentage": (
|
||||
"ModulationTempDesired",
|
||||
PERCENTAGE,
|
||||
"mdi:percent",
|
||||
0,
|
||||
None,
|
||||
],
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -11,7 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import Throttle, dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import EbusdData
|
||||
from .const import EBUSD_DATA, SensorSpecs
|
||||
|
||||
TIME_FRAME1_BEGIN = "time_frame1_begin"
|
||||
TIME_FRAME1_END = "time_frame1_end"
|
||||
@@ -33,9 +35,9 @@ def setup_platform(
|
||||
"""Set up the Ebus sensor."""
|
||||
if not discovery_info:
|
||||
return
|
||||
ebusd_api = hass.data[DOMAIN]
|
||||
monitored_conditions = discovery_info["monitored_conditions"]
|
||||
name = discovery_info["client_name"]
|
||||
ebusd_api = hass.data[EBUSD_DATA]
|
||||
monitored_conditions: list[str] = discovery_info["monitored_conditions"]
|
||||
name: str = discovery_info["client_name"]
|
||||
|
||||
add_entities(
|
||||
(
|
||||
@@ -49,9 +51,8 @@ def setup_platform(
|
||||
class EbusdSensor(SensorEntity):
|
||||
"""Ebusd component sensor methods definition."""
|
||||
|
||||
def __init__(self, data, sensor, name):
|
||||
def __init__(self, data: EbusdData, sensor: SensorSpecs, name: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._state = None
|
||||
self._client_name = name
|
||||
(
|
||||
self._name,
|
||||
@@ -63,20 +64,15 @@ class EbusdSensor(SensorEntity):
|
||||
self.data = data
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._client_name} {self._name}"
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return the device state attributes."""
|
||||
if self._type == 1 and self._state is not None:
|
||||
schedule = {
|
||||
if self._type == 1 and (native_value := self.native_value) is not None:
|
||||
schedule: dict[str, str | None] = {
|
||||
TIME_FRAME1_BEGIN: None,
|
||||
TIME_FRAME1_END: None,
|
||||
TIME_FRAME2_BEGIN: None,
|
||||
@@ -84,7 +80,7 @@ class EbusdSensor(SensorEntity):
|
||||
TIME_FRAME3_BEGIN: None,
|
||||
TIME_FRAME3_END: None,
|
||||
}
|
||||
time_frame = self._state.split(";")
|
||||
time_frame = cast(str, native_value).split(";")
|
||||
for index, item in enumerate(sorted(schedule.items())):
|
||||
if index < len(time_frame):
|
||||
parsed = datetime.datetime.strptime(time_frame[index], "%H:%M")
|
||||
@@ -101,12 +97,12 @@ class EbusdSensor(SensorEntity):
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
def icon(self) -> str | None:
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@@ -118,6 +114,6 @@ class EbusdSensor(SensorEntity):
|
||||
if self._name not in self.data.value:
|
||||
return
|
||||
|
||||
self._state = self.data.value[self._name]
|
||||
self._attr_native_value = self.data.value[self._name]
|
||||
except RuntimeError:
|
||||
_LOGGER.debug("EbusdData.update exception")
|
||||
|
||||
36
homeassistant/components/homevolt/__init__.py
Normal file
36
homeassistant/components/homevolt/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""The Homevolt integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homevolt import Homevolt
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import HomevoltConfigEntry, HomevoltDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: HomevoltConfigEntry) -> bool:
|
||||
"""Set up Homevolt from a config entry."""
|
||||
host: str = entry.data[CONF_HOST]
|
||||
password: str | None = entry.data.get(CONF_PASSWORD)
|
||||
|
||||
websession = async_get_clientsession(hass)
|
||||
client = Homevolt(host, password, websession=websession)
|
||||
|
||||
coordinator = HomevoltDataUpdateCoordinator(hass, entry, client)
|
||||
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: HomevoltConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
70
homeassistant/components/homevolt/config_flow.py
Normal file
70
homeassistant/components/homevolt/config_flow.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Config flow for the Homevolt integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homevolt import Homevolt, HomevoltAuthenticationError, HomevoltConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Optional(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class HomevoltConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Homevolt."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
password = user_input.get(CONF_PASSWORD)
|
||||
websession = async_get_clientsession(self.hass)
|
||||
client = Homevolt(host, password, websession=websession)
|
||||
try:
|
||||
await client.update_info()
|
||||
device = client.get_device()
|
||||
device_id = device.device_id
|
||||
except HomevoltAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except HomevoltConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Error occurred while connecting to the Homevolt battery"
|
||||
)
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(device_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title="Homevolt Local",
|
||||
data={
|
||||
CONF_HOST: host,
|
||||
CONF_PASSWORD: password,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
9
homeassistant/components/homevolt/const.py
Normal file
9
homeassistant/components/homevolt/const.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Constants for the Homevolt integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
DOMAIN = "homevolt"
|
||||
MANUFACTURER = "Homevolt"
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
56
homeassistant/components/homevolt/coordinator.py
Normal file
56
homeassistant/components/homevolt/coordinator.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Data update coordinator for Homevolt integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homevolt import (
|
||||
Device,
|
||||
Homevolt,
|
||||
HomevoltAuthenticationError,
|
||||
HomevoltConnectionError,
|
||||
HomevoltError,
|
||||
)
|
||||
|
||||
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 .const import DOMAIN, SCAN_INTERVAL
|
||||
|
||||
type HomevoltConfigEntry = ConfigEntry[HomevoltDataUpdateCoordinator]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HomevoltDataUpdateCoordinator(DataUpdateCoordinator[Device]):
|
||||
"""Class to manage fetching Homevolt data."""
|
||||
|
||||
config_entry: HomevoltConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: HomevoltConfigEntry,
|
||||
client: Homevolt,
|
||||
) -> None:
|
||||
"""Initialize the Homevolt coordinator."""
|
||||
self.client = client
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=entry,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> Device:
|
||||
"""Fetch data from the Homevolt API."""
|
||||
try:
|
||||
await self.client.update_info()
|
||||
return self.client.get_device()
|
||||
except HomevoltAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except (HomevoltConnectionError, HomevoltError) as err:
|
||||
raise UpdateFailed(f"Error communicating with device: {err}") from err
|
||||
12
homeassistant/components/homevolt/manifest.json
Normal file
12
homeassistant/components/homevolt/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "homevolt",
|
||||
"name": "Homevolt",
|
||||
"codeowners": ["@danielhiversen"],
|
||||
"config_flow": true,
|
||||
"dependencies": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homevolt",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["homevolt==0.2.4"]
|
||||
}
|
||||
70
homeassistant/components/homevolt/quality_scale.yaml
Normal file
70
homeassistant/components/homevolt/quality_scale.yaml
Normal file
@@ -0,0 +1,70 @@
|
||||
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: Local_polling without 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:
|
||||
status: exempt
|
||||
comment: Integration does not register custom actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: Integration does not have an options flow.
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
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: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
||||
162
homeassistant/components/homevolt/sensor.py
Normal file
162
homeassistant/components/homevolt/sensor.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""Support for Homevolt sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homevolt.models import SensorType
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfFrequency,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import HomevoltConfigEntry, HomevoltDataUpdateCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0 # Coordinator-based updates
|
||||
|
||||
SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key=SensorType.COUNT,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SensorType.ENERGY_TOTAL,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SensorType.ENERGY_INCREASING,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SensorType.FREQUENCY,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SensorType.PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SensorType.POWER,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SensorType.SCHEDULE_TYPE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SensorType.SIGNAL_STRENGTH,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SensorType.TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SensorType.TEXT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SensorType.VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SensorType.CURRENT,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HomevoltConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Homevolt sensor."""
|
||||
coordinator = entry.runtime_data
|
||||
entities = []
|
||||
sensors_by_key = {sensor.key: sensor for sensor in SENSORS}
|
||||
for sensor_key, sensor in coordinator.data.sensors.items():
|
||||
if (description := sensors_by_key.get(sensor.type)) is None:
|
||||
continue
|
||||
entities.append(
|
||||
HomevoltSensor(
|
||||
description,
|
||||
coordinator,
|
||||
sensor_key,
|
||||
)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HomevoltSensor(CoordinatorEntity[HomevoltDataUpdateCoordinator], SensorEntity):
|
||||
"""Representation of a Homevolt sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
description: SensorEntityDescription,
|
||||
coordinator: HomevoltDataUpdateCoordinator,
|
||||
sensor_key: str,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
device_id = coordinator.data.device_id
|
||||
self._attr_unique_id = f"{device_id}_{sensor_key}"
|
||||
sensor_data = coordinator.data.sensors[sensor_key]
|
||||
self._attr_translation_key = sensor_data.slug
|
||||
self._sensor_key = sensor_key
|
||||
device_metadata = coordinator.data.device_metadata.get(
|
||||
sensor_data.device_identifier
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{device_id}_{sensor_data.device_identifier}")},
|
||||
configuration_url=coordinator.client.base_url,
|
||||
manufacturer=MANUFACTURER,
|
||||
model=device_metadata.model if device_metadata else None,
|
||||
name=device_metadata.name if device_metadata else None,
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self._sensor_key in self.coordinator.data.sensors
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the native value of the sensor."""
|
||||
return self.coordinator.data.sensors[self._sensor_key].value
|
||||
198
homeassistant/components/homevolt/strings.json
Normal file
198
homeassistant/components/homevolt/strings.json
Normal file
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address or hostname of your Homevolt battery on your local network.",
|
||||
"password": "The local password configured for your Homevolt battery, if required."
|
||||
},
|
||||
"description": "Connect Home Assistant to your Homevolt battery over the local network.",
|
||||
"title": "Homevolt Local"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"available_charging_energy": {
|
||||
"name": "Available charging energy"
|
||||
},
|
||||
"available_charging_power": {
|
||||
"name": "Available charging power"
|
||||
},
|
||||
"available_discharge_energy": {
|
||||
"name": "Available discharge energy"
|
||||
},
|
||||
"available_discharge_power": {
|
||||
"name": "Available discharge power"
|
||||
},
|
||||
"average_rssi_grid": {
|
||||
"name": "Grid average RSSI"
|
||||
},
|
||||
"average_rssi_load": {
|
||||
"name": "Load average RSSI"
|
||||
},
|
||||
"battery_state_of_charge": {
|
||||
"name": "Battery state of charge"
|
||||
},
|
||||
"charge_cycles": {
|
||||
"name": "Charge cycles"
|
||||
},
|
||||
"energy_exported_grid": {
|
||||
"name": "Grid exported energy"
|
||||
},
|
||||
"energy_exported_load": {
|
||||
"name": "Load exported energy"
|
||||
},
|
||||
"energy_imported_grid": {
|
||||
"name": "Grid imported energy"
|
||||
},
|
||||
"energy_imported_load": {
|
||||
"name": "Load imported energy"
|
||||
},
|
||||
"exported_energy": {
|
||||
"name": "Exported energy"
|
||||
},
|
||||
"frequency": {
|
||||
"name": "Frequency"
|
||||
},
|
||||
"imported_energy": {
|
||||
"name": "Imported energy"
|
||||
},
|
||||
"l1_current": {
|
||||
"name": "L1 current"
|
||||
},
|
||||
"l1_current_grid": {
|
||||
"name": "Grid L1 current"
|
||||
},
|
||||
"l1_current_load": {
|
||||
"name": "Load L1 current"
|
||||
},
|
||||
"l1_l2_voltage": {
|
||||
"name": "L1-L2 voltage"
|
||||
},
|
||||
"l1_power_grid": {
|
||||
"name": "Grid L1 power"
|
||||
},
|
||||
"l1_power_load": {
|
||||
"name": "Load L1 power"
|
||||
},
|
||||
"l1_voltage": {
|
||||
"name": "L1 voltage"
|
||||
},
|
||||
"l1_voltage_grid": {
|
||||
"name": "Grid L1 voltage"
|
||||
},
|
||||
"l1_voltage_load": {
|
||||
"name": "Load L1 voltage"
|
||||
},
|
||||
"l2_current": {
|
||||
"name": "L2 current"
|
||||
},
|
||||
"l2_current_grid": {
|
||||
"name": "Grid L2 current"
|
||||
},
|
||||
"l2_current_load": {
|
||||
"name": "Load L2 current"
|
||||
},
|
||||
"l2_l3_voltage": {
|
||||
"name": "L2-L3 voltage"
|
||||
},
|
||||
"l2_power_grid": {
|
||||
"name": "Grid L2 power"
|
||||
},
|
||||
"l2_power_load": {
|
||||
"name": "Load L2 power"
|
||||
},
|
||||
"l2_voltage": {
|
||||
"name": "L2 voltage"
|
||||
},
|
||||
"l2_voltage_grid": {
|
||||
"name": "Grid L2 voltage"
|
||||
},
|
||||
"l2_voltage_load": {
|
||||
"name": "Load L2 voltage"
|
||||
},
|
||||
"l3_current": {
|
||||
"name": "L3 current"
|
||||
},
|
||||
"l3_current_grid": {
|
||||
"name": "Grid L3 current"
|
||||
},
|
||||
"l3_current_load": {
|
||||
"name": "Load L3 current"
|
||||
},
|
||||
"l3_l1_voltage": {
|
||||
"name": "L3-L1 voltage"
|
||||
},
|
||||
"l3_power_grid": {
|
||||
"name": "Grid L3 power"
|
||||
},
|
||||
"l3_power_load": {
|
||||
"name": "Load L3 power"
|
||||
},
|
||||
"l3_voltage": {
|
||||
"name": "L3 voltage"
|
||||
},
|
||||
"l3_voltage_grid": {
|
||||
"name": "Grid L3 voltage"
|
||||
},
|
||||
"l3_voltage_load": {
|
||||
"name": "Load L3 voltage"
|
||||
},
|
||||
"power": {
|
||||
"name": "Power"
|
||||
},
|
||||
"power_grid": {
|
||||
"name": "Grid power"
|
||||
},
|
||||
"power_load": {
|
||||
"name": "Load power"
|
||||
},
|
||||
"rssi_grid": {
|
||||
"name": "Grid RSSI"
|
||||
},
|
||||
"rssi_load": {
|
||||
"name": "Load RSSI"
|
||||
},
|
||||
"schedule_id": {
|
||||
"name": "Schedule ID"
|
||||
},
|
||||
"schedule_max_discharge": {
|
||||
"name": "Schedule max discharge"
|
||||
},
|
||||
"schedule_max_power": {
|
||||
"name": "Schedule max power"
|
||||
},
|
||||
"schedule_power_setpoint": {
|
||||
"name": "Schedule power setpoint"
|
||||
},
|
||||
"schedule_type": {
|
||||
"name": "Schedule type"
|
||||
},
|
||||
"state_of_charge": {
|
||||
"name": "State of charge"
|
||||
},
|
||||
"system_temperature": {
|
||||
"name": "System temperature"
|
||||
},
|
||||
"tmax": {
|
||||
"name": "Maximum temperature"
|
||||
},
|
||||
"tmin": {
|
||||
"name": "Minimum temperature"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +181,7 @@ class GenericHueSensor(GenericHueDevice, entity.Entity): # pylint: disable=hass
|
||||
)
|
||||
|
||||
@property
|
||||
def state_class(self):
|
||||
def state_class(self) -> SensorStateClass:
|
||||
"""Return the state class of this entity, from STATE_CLASSES, if any."""
|
||||
return SensorStateClass.MEASUREMENT
|
||||
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
is_off:
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
is_on:
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
@@ -26,3 +12,6 @@ is_on:
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from mficlient.client import FailedToLogin, MFiClient
|
||||
from mficlient.client import FailedToLogin, MFiClient, Port as MFiPort
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
StateType,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@@ -64,24 +65,29 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up mFi sensors."""
|
||||
host = config.get(CONF_HOST)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
use_tls = config.get(CONF_SSL)
|
||||
verify_tls = config.get(CONF_VERIFY_SSL)
|
||||
host: str = config[CONF_HOST]
|
||||
username: str = config[CONF_USERNAME]
|
||||
password: str = config[CONF_PASSWORD]
|
||||
use_tls: bool = config[CONF_SSL]
|
||||
verify_tls: bool = config[CONF_VERIFY_SSL]
|
||||
default_port = 6443 if use_tls else 6080
|
||||
port = int(config.get(CONF_PORT, default_port))
|
||||
network_port: int = config.get(CONF_PORT, default_port)
|
||||
|
||||
try:
|
||||
client = MFiClient(
|
||||
host, username, password, port=port, use_tls=use_tls, verify=verify_tls
|
||||
host,
|
||||
username,
|
||||
password,
|
||||
port=network_port,
|
||||
use_tls=use_tls,
|
||||
verify=verify_tls,
|
||||
)
|
||||
except (FailedToLogin, requests.exceptions.ConnectionError) as ex:
|
||||
_LOGGER.error("Unable to connect to mFi: %s", str(ex))
|
||||
return
|
||||
|
||||
add_entities(
|
||||
MfiSensor(port, hass)
|
||||
MfiSensor(port)
|
||||
for device in client.get_devices()
|
||||
for port in device.ports.values()
|
||||
if port.model in SENSOR_MODELS
|
||||
@@ -91,18 +97,17 @@ def setup_platform(
|
||||
class MfiSensor(SensorEntity):
|
||||
"""Representation of a mFi sensor."""
|
||||
|
||||
def __init__(self, port, hass):
|
||||
def __init__(self, port: MFiPort) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._port = port
|
||||
self._hass = hass
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
return self._port.label
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
try:
|
||||
tag = self._port.tag
|
||||
@@ -129,7 +134,7 @@ class MfiSensor(SensorEntity):
|
||||
return None
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
try:
|
||||
tag = self._port.tag
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from mficlient.client import FailedToLogin, MFiClient
|
||||
from mficlient.client import FailedToLogin, MFiClient, Port as MFiPort
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -51,18 +51,23 @@ def setup_platform(
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up mFi sensors."""
|
||||
host = config.get(CONF_HOST)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
use_tls = config[CONF_SSL]
|
||||
verify_tls = config.get(CONF_VERIFY_SSL)
|
||||
"""Set up mFi switches."""
|
||||
host: str = config[CONF_HOST]
|
||||
username: str = config[CONF_USERNAME]
|
||||
password: str = config[CONF_PASSWORD]
|
||||
use_tls: bool = config[CONF_SSL]
|
||||
verify_tls: bool = config[CONF_VERIFY_SSL]
|
||||
default_port = 6443 if use_tls else 6080
|
||||
port = int(config.get(CONF_PORT, default_port))
|
||||
network_port: int = config.get(CONF_PORT, default_port)
|
||||
|
||||
try:
|
||||
client = MFiClient(
|
||||
host, username, password, port=port, use_tls=use_tls, verify=verify_tls
|
||||
host,
|
||||
username,
|
||||
password,
|
||||
port=network_port,
|
||||
use_tls=use_tls,
|
||||
verify=verify_tls,
|
||||
)
|
||||
except (FailedToLogin, requests.exceptions.ConnectionError) as ex:
|
||||
_LOGGER.error("Unable to connect to mFi: %s", str(ex))
|
||||
@@ -79,23 +84,23 @@ def setup_platform(
|
||||
class MfiSwitch(SwitchEntity):
|
||||
"""Representation of an mFi switch-able device."""
|
||||
|
||||
def __init__(self, port):
|
||||
def __init__(self, port: MFiPort) -> None:
|
||||
"""Initialize the mFi device."""
|
||||
self._port = port
|
||||
self._target_state = None
|
||||
self._target_state: bool | None = None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID of the device."""
|
||||
return self._port.ident
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name of the device."""
|
||||
return self._port.label
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the device is on."""
|
||||
return self._port.output
|
||||
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.0.17"]
|
||||
"requirements": ["onedrive-personal-sdk==0.1.0"]
|
||||
}
|
||||
|
||||
@@ -112,45 +112,49 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
self._async_abort_entries_match(user_input)
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except openai.APIConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except openai.AuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="ChatGPT",
|
||||
data=user_input,
|
||||
subentries=[
|
||||
{
|
||||
"subentry_type": "conversation",
|
||||
"data": RECOMMENDED_CONVERSATION_OPTIONS,
|
||||
"title": DEFAULT_CONVERSATION_NAME,
|
||||
"unique_id": None,
|
||||
},
|
||||
{
|
||||
"subentry_type": "ai_task_data",
|
||||
"data": RECOMMENDED_AI_TASK_OPTIONS,
|
||||
"title": DEFAULT_AI_TASK_NAME,
|
||||
"unique_id": None,
|
||||
},
|
||||
],
|
||||
)
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(user_input)
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except openai.APIConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except openai.AuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="ChatGPT",
|
||||
data=user_input,
|
||||
subentries=[
|
||||
{
|
||||
"subentry_type": "conversation",
|
||||
"data": RECOMMENDED_CONVERSATION_OPTIONS,
|
||||
"title": DEFAULT_CONVERSATION_NAME,
|
||||
"unique_id": None,
|
||||
},
|
||||
{
|
||||
"subentry_type": "ai_task_data",
|
||||
"data": RECOMMENDED_AI_TASK_OPTIONS,
|
||||
"title": DEFAULT_AI_TASK_NAME,
|
||||
"unique_id": None,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, user_input
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"instructions_url": "https://www.home-assistant.io/integrations/openai_conversation/#generate-an-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "Your OpenAI API key."
|
||||
},
|
||||
"description": "Set up OpenAI Conversation integration by providing your OpenAI API key. Instructions to obtain an API key can be found [here]({instructions_url})."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sunricher_dali",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["PySrDaliGateway==0.18.0"]
|
||||
"requirements": ["PySrDaliGateway==0.19.3"]
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
"""Provides conditions for switches."""
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the switch conditions."""
|
||||
return CONDITIONS
|
||||
@@ -1,17 +0,0 @@
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
entity:
|
||||
domain: switch
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_off": {
|
||||
"condition": "mdi:toggle-switch-variant-off"
|
||||
},
|
||||
"is_on": {
|
||||
"condition": "mdi:toggle-switch-variant"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:toggle-switch-variant",
|
||||
|
||||
@@ -1,32 +1,8 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted switches.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted switches to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_off": {
|
||||
"description": "Tests if one or more switches are off.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::switch::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::switch::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "If a switch is off"
|
||||
},
|
||||
"is_on": {
|
||||
"description": "Tests if one or more switches are on.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::switch::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::switch::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "If a switch is on"
|
||||
}
|
||||
},
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"toggle": "[%key:common::device_automation::action_type::toggle%]",
|
||||
@@ -70,12 +46,6 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
from typing import Final
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from tesla_fleet_api.const import Scope
|
||||
from tesla_fleet_api.exceptions import (
|
||||
Forbidden,
|
||||
@@ -27,6 +27,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
@@ -75,28 +76,48 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def _get_access_token(oauth_session: OAuth2Session) -> str:
|
||||
"""Get a valid access token, refreshing if necessary."""
|
||||
LOGGER.debug(
|
||||
"Token valid: %s, expires_at: %s",
|
||||
oauth_session.valid_token,
|
||||
oauth_session.token.get("expires_at"),
|
||||
)
|
||||
try:
|
||||
await oauth_session.async_ensure_token_valid()
|
||||
except ClientResponseError as err:
|
||||
if err.status == 401:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise ConfigEntryNotReady from err
|
||||
except (KeyError, TypeError) as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="token_data_malformed",
|
||||
) from err
|
||||
except ClientError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
return oauth_session.token[CONF_ACCESS_TOKEN]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -> bool:
|
||||
"""Set up Teslemetry config."""
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth_implementation_not_available",
|
||||
) from err
|
||||
oauth_session = OAuth2Session(hass, entry, implementation)
|
||||
|
||||
async def _get_access_token() -> str:
|
||||
try:
|
||||
await oauth_session.async_ensure_token_valid()
|
||||
except ClientResponseError as e:
|
||||
if e.status == 401:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
raise ConfigEntryNotReady from e
|
||||
token: str = oauth_session.token[CONF_ACCESS_TOKEN]
|
||||
return token
|
||||
|
||||
# Create API connection
|
||||
access_token = partial(_get_access_token, oauth_session)
|
||||
teslemetry = Teslemetry(
|
||||
session=session,
|
||||
access_token=_get_access_token,
|
||||
access_token=access_token,
|
||||
)
|
||||
try:
|
||||
calls = await asyncio.gather(
|
||||
@@ -154,7 +175,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
|
||||
if not stream:
|
||||
stream = TeslemetryStream(
|
||||
session,
|
||||
_get_access_token,
|
||||
access_token,
|
||||
server=f"{region.lower()}.teslemetry.com",
|
||||
parse_timestamp=True,
|
||||
manual=True,
|
||||
|
||||
@@ -997,7 +997,6 @@
|
||||
"total_grid_energy_exported": {
|
||||
"name": "Grid exported"
|
||||
},
|
||||
|
||||
"total_home_usage": {
|
||||
"name": "Home usage"
|
||||
},
|
||||
@@ -1127,6 +1126,9 @@
|
||||
"no_vehicle_data_for_device": {
|
||||
"message": "No vehicle data for device ID: {device_id}"
|
||||
},
|
||||
"oauth_implementation_not_available": {
|
||||
"message": "OAuth implementation not available, try reauthenticating"
|
||||
},
|
||||
"set_scheduled_charging_time": {
|
||||
"message": "Scheduled charging time is required when enabling"
|
||||
},
|
||||
@@ -1136,6 +1138,9 @@
|
||||
"set_scheduled_departure_preconditioning": {
|
||||
"message": "Preconditioning departure time is required when enabling"
|
||||
},
|
||||
"token_data_malformed": {
|
||||
"message": "Token data malformed, try reauthenticating"
|
||||
},
|
||||
"wake_up_failed": {
|
||||
"message": "Failed to wake up vehicle: {message}"
|
||||
},
|
||||
|
||||
@@ -9,8 +9,8 @@ import voluptuous as vol
|
||||
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, CONF_UNIQUE_ID, Platform
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
discovery,
|
||||
@@ -20,7 +20,6 @@ from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.helper_integration import (
|
||||
async_handle_source_entity_changes,
|
||||
async_remove_helper_config_entry_from_source_device,
|
||||
@@ -44,9 +43,8 @@ from .const import (
|
||||
DATA_UTILITY,
|
||||
DOMAIN,
|
||||
METER_TYPES,
|
||||
SERVICE_RESET,
|
||||
SIGNAL_RESET_METER,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -120,27 +118,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up an Utility Meter."""
|
||||
hass.data[DATA_UTILITY] = {}
|
||||
|
||||
async def async_reset_meters(service_call):
|
||||
"""Reset all sensors of a meter."""
|
||||
meters = service_call.data["entity_id"]
|
||||
|
||||
for meter in meters:
|
||||
_LOGGER.debug("resetting meter %s", meter)
|
||||
domain, entity = split_entity_id(meter)
|
||||
# backward compatibility up to 2022.07:
|
||||
if domain == DOMAIN:
|
||||
async_dispatcher_send(
|
||||
hass, SIGNAL_RESET_METER, f"{SELECT_DOMAIN}.{entity}"
|
||||
)
|
||||
else:
|
||||
async_dispatcher_send(hass, SIGNAL_RESET_METER, meter)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_RESET,
|
||||
async_reset_meters,
|
||||
vol.Schema({ATTR_ENTITY_ID: vol.All(cv.ensure_list, [cv.entity_id])}),
|
||||
)
|
||||
async_setup_services(hass)
|
||||
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
@@ -700,7 +700,7 @@ class UtilityMeterSensor(RestoreSensor):
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_class(self):
|
||||
def state_class(self) -> SensorStateClass:
|
||||
"""Return the device class of the sensor."""
|
||||
return (
|
||||
SensorStateClass.TOTAL
|
||||
|
||||
43
homeassistant/components/utility_meter/services.py
Normal file
43
homeassistant/components/utility_meter/services.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Support for tracking consumption over given periods of time."""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback, split_entity_id
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import DOMAIN, SERVICE_RESET, SIGNAL_RESET_METER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_reset_meters(service_call: ServiceCall) -> None:
|
||||
"""Reset all sensors of a meter."""
|
||||
meters = service_call.data["entity_id"]
|
||||
|
||||
for meter in meters:
|
||||
_LOGGER.debug("resetting meter %s", meter)
|
||||
domain, entity = split_entity_id(meter)
|
||||
# backward compatibility up to 2022.07:
|
||||
if domain == DOMAIN:
|
||||
async_dispatcher_send(
|
||||
service_call.hass, SIGNAL_RESET_METER, f"{SELECT_DOMAIN}.{entity}"
|
||||
)
|
||||
else:
|
||||
async_dispatcher_send(service_call.hass, SIGNAL_RESET_METER, meter)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_RESET,
|
||||
async_reset_meters,
|
||||
vol.Schema({ATTR_ENTITY_ID: vol.All(cv.ensure_list, [cv.entity_id])}),
|
||||
)
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -294,6 +294,7 @@ FLOWS = {
|
||||
"homekit",
|
||||
"homekit_controller",
|
||||
"homematicip_cloud",
|
||||
"homevolt",
|
||||
"homewizard",
|
||||
"homeworks",
|
||||
"honeywell",
|
||||
|
||||
@@ -2836,6 +2836,12 @@
|
||||
"zwave"
|
||||
]
|
||||
},
|
||||
"homevolt": {
|
||||
"name": "Homevolt",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"homewizard": {
|
||||
"name": "HomeWizard",
|
||||
"integration_type": "device",
|
||||
|
||||
@@ -700,6 +700,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [
|
||||
TypeHintMatch(
|
||||
function_name="device_class",
|
||||
return_type=["str", None],
|
||||
mandatory=True,
|
||||
),
|
||||
TypeHintMatch(
|
||||
function_name="unit_of_measurement",
|
||||
@@ -2518,10 +2519,12 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = {
|
||||
TypeHintMatch(
|
||||
function_name="state_class",
|
||||
return_type=["SensorStateClass", "str", None],
|
||||
mandatory=True,
|
||||
),
|
||||
TypeHintMatch(
|
||||
function_name="last_reset",
|
||||
return_type=["datetime", None],
|
||||
mandatory=True,
|
||||
),
|
||||
TypeHintMatch(
|
||||
function_name="native_value",
|
||||
|
||||
7
requirements_all.txt
generated
7
requirements_all.txt
generated
@@ -80,7 +80,7 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.sunricher_dali
|
||||
PySrDaliGateway==0.18.0
|
||||
PySrDaliGateway==0.19.3
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.76.0
|
||||
@@ -1226,6 +1226,9 @@ homelink-integration-api==0.0.1
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.4.0
|
||||
|
||||
# homeassistant.components.homevolt
|
||||
homevolt==0.2.4
|
||||
|
||||
# homeassistant.components.horizon
|
||||
horimote==0.4.1
|
||||
|
||||
@@ -1646,7 +1649,7 @@ omnilogic==0.4.5
|
||||
ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
onedrive-personal-sdk==0.0.17
|
||||
onedrive-personal-sdk==0.1.0
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
|
||||
7
requirements_test_all.txt
generated
7
requirements_test_all.txt
generated
@@ -80,7 +80,7 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.sunricher_dali
|
||||
PySrDaliGateway==0.18.0
|
||||
PySrDaliGateway==0.19.3
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.76.0
|
||||
@@ -1084,6 +1084,9 @@ homelink-integration-api==0.0.1
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.4.0
|
||||
|
||||
# homeassistant.components.homevolt
|
||||
homevolt==0.2.4
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
httplib2==0.20.4
|
||||
|
||||
@@ -1429,7 +1432,7 @@ omnilogic==0.4.5
|
||||
ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
onedrive-personal-sdk==0.0.17
|
||||
onedrive-personal-sdk==0.1.0
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
|
||||
1
tests/components/homevolt/__init__.py
Normal file
1
tests/components/homevolt/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the Homevolt integration."""
|
||||
110
tests/components/homevolt/conftest.py
Normal file
110
tests/components/homevolt/conftest.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Common fixtures for the Homevolt tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from homevolt import Device, DeviceMetadata, Sensor, SensorType
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homevolt.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.homevolt.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(
|
||||
title="Homevolt",
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
unique_id="40580137858664",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_homevolt_client() -> Generator[MagicMock]:
|
||||
"""Return a mocked Homevolt client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homevolt.Homevolt",
|
||||
autospec=True,
|
||||
) as homevolt_mock,
|
||||
patch(
|
||||
"homeassistant.components.homevolt.config_flow.Homevolt",
|
||||
new=homevolt_mock,
|
||||
),
|
||||
):
|
||||
client = homevolt_mock.return_value
|
||||
client.base_url = "http://127.0.0.1"
|
||||
client.update_info = AsyncMock()
|
||||
|
||||
# Create a mock Device with sensors
|
||||
device = MagicMock(spec=Device)
|
||||
device.device_id = "40580137858664"
|
||||
device.sensors = {
|
||||
"L1 Voltage": Sensor(
|
||||
value=234.5,
|
||||
type=SensorType.VOLTAGE,
|
||||
device_identifier="ems_40580137858664",
|
||||
slug="l1_voltage",
|
||||
),
|
||||
"Battery State of Charge": Sensor(
|
||||
value=80.6,
|
||||
type=SensorType.PERCENTAGE,
|
||||
device_identifier="ems_40580137858664",
|
||||
slug="battery_state_of_charge",
|
||||
),
|
||||
"Power": Sensor(
|
||||
value=-12,
|
||||
type=SensorType.POWER,
|
||||
device_identifier="ems_40580137858664",
|
||||
slug="power",
|
||||
),
|
||||
}
|
||||
device.device_metadata = {
|
||||
"ems_40580137858664": DeviceMetadata(
|
||||
name="Homevolt EMS",
|
||||
model="EMS-1000",
|
||||
),
|
||||
}
|
||||
client.get_device.return_value = device
|
||||
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Return the platforms to test."""
|
||||
return [Platform.SENSOR]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_homevolt_client: MagicMock,
|
||||
platforms: list[Platform],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the Homevolt integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.homevolt.PLATFORMS", platforms):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
169
tests/components/homevolt/snapshots/test_sensor.ambr
Normal file
169
tests/components/homevolt/snapshots/test_sensor.ambr
Normal file
@@ -0,0 +1,169 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[sensor.homevolt_ems_battery_state_of_charge-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'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.homevolt_ems_battery_state_of_charge',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Battery state of charge',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery state of charge',
|
||||
'platform': 'homevolt',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'battery_state_of_charge',
|
||||
'unique_id': '40580137858664_Battery State of Charge',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.homevolt_ems_battery_state_of_charge-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'Homevolt EMS Battery state of charge',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.homevolt_ems_battery_state_of_charge',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '80.6',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.homevolt_ems_l1_voltage-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'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.homevolt_ems_l1_voltage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'L1 voltage',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'L1 voltage',
|
||||
'platform': 'homevolt',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'l1_voltage',
|
||||
'unique_id': '40580137858664_L1 Voltage',
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.homevolt_ems_l1_voltage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'voltage',
|
||||
'friendly_name': 'Homevolt EMS L1 voltage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.homevolt_ems_l1_voltage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '234.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.homevolt_ems_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'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.homevolt_ems_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Power',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power',
|
||||
'platform': 'homevolt',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'power',
|
||||
'unique_id': '40580137858664_Power',
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.homevolt_ems_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Homevolt EMS Power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.homevolt_ems_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '-12',
|
||||
})
|
||||
# ---
|
||||
170
tests/components/homevolt/test_config_flow.py
Normal file
170
tests/components/homevolt/test_config_flow.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""Tests for the Homevolt config flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from homevolt import HomevoltAuthenticationError, HomevoltConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homevolt.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_full_flow_success(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test a complete successful user flow."""
|
||||
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"] == {}
|
||||
|
||||
user_input = {
|
||||
CONF_HOST: "192.168.1.100",
|
||||
CONF_PASSWORD: "test-password",
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homevolt.config_flow.Homevolt.update_info",
|
||||
new_callable=AsyncMock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homevolt.config_flow.Homevolt.get_device",
|
||||
) as mock_get_device,
|
||||
):
|
||||
mock_device = MagicMock()
|
||||
mock_device.device_id = "40580137858664"
|
||||
mock_get_device.return_value = mock_device
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Homevolt Local"
|
||||
assert result["data"] == user_input
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_error"),
|
||||
[
|
||||
(HomevoltAuthenticationError, "invalid_auth"),
|
||||
(HomevoltConnectionError, "cannot_connect"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_step_user_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
exception: Exception,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
"""Test error cases for the user step with recovery."""
|
||||
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"] == {}
|
||||
|
||||
user_input = {
|
||||
CONF_HOST: "192.168.1.100",
|
||||
CONF_PASSWORD: "test-password",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homevolt.config_flow.Homevolt.update_info",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_update_info:
|
||||
mock_update_info.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homevolt.config_flow.Homevolt.update_info",
|
||||
new_callable=AsyncMock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homevolt.config_flow.Homevolt.get_device",
|
||||
) as mock_get_device,
|
||||
):
|
||||
mock_device = MagicMock()
|
||||
mock_device.device_id = "40580137858664"
|
||||
mock_get_device.return_value = mock_device
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Homevolt Local"
|
||||
assert result["data"] == user_input
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_duplicate_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that a duplicate device_id aborts the flow."""
|
||||
existing_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: "192.168.1.100", CONF_PASSWORD: "test-password"},
|
||||
unique_id="40580137858664",
|
||||
)
|
||||
existing_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"
|
||||
assert result["errors"] == {}
|
||||
|
||||
user_input = {
|
||||
CONF_HOST: "192.168.1.200",
|
||||
CONF_PASSWORD: "test-password",
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homevolt.config_flow.Homevolt.update_info",
|
||||
new_callable=AsyncMock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homevolt.config_flow.Homevolt.get_device",
|
||||
) as mock_get_device,
|
||||
):
|
||||
mock_device = MagicMock()
|
||||
mock_device.device_id = "40580137858664"
|
||||
mock_get_device.return_value = mock_device
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
65
tests/components/homevolt/test_init.py
Normal file
65
tests/components/homevolt/test_init.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Test the Homevolt init module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homevolt import HomevoltAuthenticationError, HomevoltConnectionError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_load_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_homevolt_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test load and unload entry."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_remove(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_config_entry_not_ready(
|
||||
hass: HomeAssistant,
|
||||
mock_homevolt_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Homevolt configuration entry not ready."""
|
||||
mock_homevolt_client.update_info.side_effect = HomevoltConnectionError(
|
||||
"Connection failed"
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_config_entry_auth_failed(
|
||||
hass: HomeAssistant,
|
||||
mock_homevolt_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Homevolt configuration entry authentication failed."""
|
||||
mock_homevolt_client.update_info.side_effect = HomevoltAuthenticationError(
|
||||
"Authentication failed"
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
60
tests/components/homevolt/test_sensor.py
Normal file
60
tests/components/homevolt/test_sensor.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Tests for the Homevolt sensor platform."""
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.homevolt.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
pytestmark = pytest.mark.usefixtures(
|
||||
"entity_registry_enabled_by_default", "init_integration"
|
||||
)
|
||||
|
||||
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the sensor entities."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, "40580137858664_ems_40580137858664")}
|
||||
)
|
||||
assert device_entry
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
for entity_entry in entity_entries:
|
||||
assert entity_entry.device_id == device_entry.id
|
||||
|
||||
|
||||
async def test_sensor_exposes_values_from_coordinator(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_homevolt_client,
|
||||
) -> None:
|
||||
"""Ensure sensor entities are created and expose values from the coordinator."""
|
||||
unique_id = "40580137858664_L1 Voltage"
|
||||
entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, unique_id)
|
||||
assert entity_id is not None
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert float(state.state) == 234.5
|
||||
|
||||
mock_homevolt_client.get_device.return_value.sensors["L1 Voltage"].value = 240.1
|
||||
coordinator = mock_config_entry.runtime_data
|
||||
await coordinator.async_refresh()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert float(state.state) == 240.1
|
||||
@@ -34,7 +34,7 @@ async def test_setup_missing_config(hass: HomeAssistant) -> None:
|
||||
"""Test setup with missing configuration."""
|
||||
with mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client:
|
||||
config = {"sensor": {"platform": "mfi"}}
|
||||
assert await async_setup_component(hass, "sensor", config)
|
||||
assert await async_setup_component(hass, COMPONENT.DOMAIN, config)
|
||||
assert not mock_client.called
|
||||
|
||||
|
||||
@@ -42,14 +42,14 @@ async def test_setup_failed_login(hass: HomeAssistant) -> None:
|
||||
"""Test setup with login failure."""
|
||||
with mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client:
|
||||
mock_client.side_effect = FailedToLogin
|
||||
assert not PLATFORM.setup_platform(hass, GOOD_CONFIG, None)
|
||||
assert not PLATFORM.setup_platform(hass, GOOD_CONFIG[COMPONENT.DOMAIN], None)
|
||||
|
||||
|
||||
async def test_setup_failed_connect(hass: HomeAssistant) -> None:
|
||||
"""Test setup with connection failure."""
|
||||
with mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client:
|
||||
mock_client.side_effect = requests.exceptions.ConnectionError
|
||||
assert not PLATFORM.setup_platform(hass, GOOD_CONFIG, None)
|
||||
assert not PLATFORM.setup_platform(hass, GOOD_CONFIG[COMPONENT.DOMAIN], None)
|
||||
|
||||
|
||||
async def test_setup_minimum(hass: HomeAssistant) -> None:
|
||||
@@ -111,7 +111,7 @@ async def test_setup_adds_proper_devices(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
for ident, port in ports.items():
|
||||
if ident != "bad":
|
||||
mock_sensor.assert_any_call(port, hass)
|
||||
mock_sensor.assert_any_call(port)
|
||||
assert mock.call(ports["bad"], hass) not in mock_sensor.mock_calls
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ def port_fixture() -> mock.MagicMock:
|
||||
@pytest.fixture(name="sensor")
|
||||
def sensor_fixture(hass: HomeAssistant, port: mock.MagicMock) -> mfi.MfiSensor:
|
||||
"""Sensor fixture."""
|
||||
sensor = mfi.MfiSensor(port, hass)
|
||||
sensor = mfi.MfiSensor(port)
|
||||
sensor.hass = hass
|
||||
return sensor
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
assert result["errors"] == {}
|
||||
|
||||
with (
|
||||
patch(
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
"""Test switch conditions."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components import (
|
||||
ConditionStateDescription,
|
||||
assert_condition_gated_by_labs_flag,
|
||||
create_target_condition,
|
||||
parametrize_condition_states,
|
||||
parametrize_target_entities,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_lights(hass: HomeAssistant) -> list[str]:
|
||||
"""Create multiple light entities associated with different targets."""
|
||||
return (await target_entities(hass, "light"))["included"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_switches(hass: HomeAssistant) -> list[str]:
|
||||
"""Create multiple switch entities associated with different targets."""
|
||||
return (await target_entities(hass, "switch"))["included"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"condition",
|
||||
[
|
||||
"switch.is_off",
|
||||
"switch.is_on",
|
||||
],
|
||||
)
|
||||
async def test_switch_conditions_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str
|
||||
) -> None:
|
||||
"""Test the switch conditions are gated by the labs flag."""
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("switch"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_condition_states(
|
||||
condition="switch.is_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
condition="switch.is_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_switch_state_condition_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_lights: list[str],
|
||||
target_switches: list[str],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the switch state condition with the 'any' behavior."""
|
||||
other_entity_ids = set(target_switches) - {entity_id}
|
||||
|
||||
# Set all switches, including the tested switch, to the initial state
|
||||
for eid in target_switches:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
condition = await create_target_condition(
|
||||
hass,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
)
|
||||
|
||||
# Set state for lights to ensure that they don't impact the condition
|
||||
for state in states:
|
||||
for eid in target_lights:
|
||||
set_or_remove_state(hass, eid, state["included"])
|
||||
await hass.async_block_till_done()
|
||||
assert condition(hass) is False
|
||||
|
||||
for state in states:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert condition(hass) == state["condition_true"]
|
||||
|
||||
# Check if changing other lights also passes the condition
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert condition(hass) == state["condition_true"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("switch"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_condition_states(
|
||||
condition="switch.is_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
condition="switch.is_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_switch_state_condition_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_switches: list[str],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the switch state condition with the 'all' behavior."""
|
||||
# Set state for two switches to ensure that they don't impact the condition
|
||||
hass.states.async_set("switch.label_switch_1", STATE_OFF)
|
||||
hass.states.async_set("switch.label_switch_2", STATE_ON)
|
||||
|
||||
other_entity_ids = set(target_switches) - {entity_id}
|
||||
|
||||
# Set all switches, including the tested switch, to the initial state
|
||||
for eid in target_switches:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
condition = await create_target_condition(
|
||||
hass,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
)
|
||||
|
||||
for state in states:
|
||||
included_state = state["included"]
|
||||
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
# The condition passes if all entities are either in a target state or invalid
|
||||
assert condition(hass) == (
|
||||
(not state["state_valid"])
|
||||
or (state["condition_true"] and entities_in_target == 1)
|
||||
)
|
||||
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The condition passes if all entities are either in a target state or invalid
|
||||
assert condition(hass) == (
|
||||
(not state["state_valid"]) or state["condition_true"]
|
||||
)
|
||||
@@ -443,6 +443,37 @@ async def test_migrate_from_future_version_fails(hass: HomeAssistant) -> None:
|
||||
assert entry.version == 3 # Version should remain unchanged
|
||||
|
||||
|
||||
async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None:
|
||||
"""Test that missing OAuth implementation triggers reauth."""
|
||||
mock_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
version=2,
|
||||
unique_id=UNIQUE_ID,
|
||||
data={
|
||||
"auth_implementation": DOMAIN,
|
||||
"token": {
|
||||
"access_token": "test_access_token",
|
||||
"refresh_token": "test_refresh_token",
|
||||
"expires_at": int(time.time()) + 3600,
|
||||
},
|
||||
},
|
||||
)
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
||||
# Mock the implementation lookup to raise ValueError
|
||||
with patch(
|
||||
"homeassistant.components.teslemetry.async_get_config_entry_implementation",
|
||||
side_effect=ValueError("Implementation not available"),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entry = hass.config_entries.async_get_entry(mock_entry.entry_id)
|
||||
assert entry is not None
|
||||
# Should trigger reauth, not just fail silently
|
||||
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
RETRY_EXCEPTIONS = [
|
||||
(RateLimited(data={"after": 5}), 5.0),
|
||||
(InvalidResponse(), 10.0),
|
||||
|
||||
@@ -188,12 +188,10 @@ async def test_caching(hass: HomeAssistant) -> None:
|
||||
side_effect=icon.build_resources,
|
||||
) as mock_build:
|
||||
load1 = await icon.async_get_icons(hass, "entity_component")
|
||||
# conditions, entity_component, services, triggers
|
||||
assert len(mock_build.mock_calls) == 4
|
||||
assert len(mock_build.mock_calls) == 3 # entity_component, services, triggers
|
||||
|
||||
load2 = await icon.async_get_icons(hass, "entity_component")
|
||||
# conditions, entity_component, services, triggers
|
||||
assert len(mock_build.mock_calls) == 4
|
||||
assert len(mock_build.mock_calls) == 3 # entity_component, services, triggers
|
||||
|
||||
assert load1 == load2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user