Add config flow for utility_meter (#68457)

pull/68838/head
Erik Montnemery 2022-03-29 14:46:17 +02:00 committed by GitHub
parent d81ee9b2da
commit bdb61e0222
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1141 additions and 148 deletions

View File

@ -7,10 +7,12 @@ import voluptuous as vol
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, Platform
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.helpers import discovery, entity_registry as er
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
@ -30,6 +32,8 @@ from .const import (
DATA_UTILITY,
DOMAIN,
METER_TYPES,
SERVICE_RESET,
SIGNAL_RESET_METER,
)
_LOGGER = logging.getLogger(__name__)
@ -100,6 +104,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.data[DATA_UTILITY] = {}
async def async_reset_meters(service_call):
"""Reset all sensors of a meter."""
entity_id = service_call.data["entity_id"]
domain = split_entity_id(entity_id)[0]
if domain == DOMAIN:
for entity in hass.data[DATA_LEGACY_COMPONENT].entities:
if entity_id == entity.entity_id:
_LOGGER.debug(
"forward reset meter from %s to %s",
entity_id,
entity.tracked_entity_id,
)
entity_id = entity.tracked_entity_id
_LOGGER.debug("reset meter %s", entity_id)
async_dispatcher_send(hass, SIGNAL_RESET_METER, entity_id)
hass.services.async_register(
DOMAIN,
SERVICE_RESET,
async_reset_meters,
vol.Schema({ATTR_ENTITY_ID: cv.entity_id}),
)
if DOMAIN not in config:
return True
for meter, conf in config[DOMAIN].items():
_LOGGER.debug("Setup %s.%s", DOMAIN, meter)
@ -151,3 +183,59 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Utility Meter from a config entry."""
entity_registry = er.async_get(hass)
hass.data[DATA_UTILITY][entry.entry_id] = {}
hass.data[DATA_UTILITY][entry.entry_id][DATA_TARIFF_SENSORS] = []
try:
er.async_validate_entity_id(entity_registry, entry.options[CONF_SOURCE_SENSOR])
except vol.Invalid:
# The entity is identified by an unknown entity registry ID
_LOGGER.error(
"Failed to setup utility_meter for unknown entity %s",
entry.options[CONF_SOURCE_SENSOR],
)
return False
if not entry.options.get(CONF_TARIFFS):
# Only a single meter sensor is required
hass.data[DATA_UTILITY][entry.entry_id][CONF_TARIFF_ENTITY] = None
hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,))
else:
# Create tariff selection + one meter sensor for each tariff
entity_entry = entity_registry.async_get_or_create(
Platform.SELECT, DOMAIN, entry.entry_id, suggested_object_id=entry.title
)
hass.data[DATA_UTILITY][entry.entry_id][
CONF_TARIFF_ENTITY
] = entity_entry.entity_id
hass.config_entries.async_setup_platforms(
entry, (Platform.SELECT, Platform.SENSOR)
)
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener, called when the config entry options are changed."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(
entry,
(
Platform.SELECT,
Platform.SENSOR,
),
):
hass.data[DATA_UTILITY].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,119 @@
"""Config flow for Utility Meter integration."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, cast
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
from homeassistant.helpers import selector
from homeassistant.helpers.helper_config_entry_flow import (
HelperConfigFlowHandler,
HelperFlowError,
HelperFlowFormStep,
HelperFlowMenuStep,
)
from .const import (
BIMONTHLY,
CONF_METER_DELTA_VALUES,
CONF_METER_NET_CONSUMPTION,
CONF_METER_OFFSET,
CONF_METER_TYPE,
CONF_SOURCE_SENSOR,
CONF_TARIFFS,
DAILY,
DOMAIN,
HOURLY,
MONTHLY,
QUARTER_HOURLY,
QUARTERLY,
WEEKLY,
YEARLY,
)
METER_TYPES = [
{"value": "none", "label": "No cycle"},
{"value": QUARTER_HOURLY, "label": "Every 15 minutes"},
{"value": HOURLY, "label": "Hourly"},
{"value": DAILY, "label": "Daily"},
{"value": WEEKLY, "label": "Weekly"},
{"value": MONTHLY, "label": "Monthly"},
{"value": BIMONTHLY, "label": "Every two months"},
{"value": QUARTERLY, "label": "Quarterly"},
{"value": YEARLY, "label": "Yearly"},
]
def _validate_config(data: Any) -> Any:
"""Validate config."""
tariffs: list[str]
if not data[CONF_TARIFFS]:
tariffs = []
else:
tariffs = data[CONF_TARIFFS].split(",")
try:
vol.Unique()(tariffs)
except vol.Invalid as exc:
raise HelperFlowError("tariffs_not_unique") from exc
return data
OPTIONS_SCHEMA = vol.Schema(
{
vol.Required(CONF_SOURCE_SENSOR): selector.selector(
{"entity": {"domain": "sensor"}},
),
}
)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): selector.selector({"text": {}}),
vol.Required(CONF_SOURCE_SENSOR): selector.selector(
{"entity": {"domain": "sensor"}},
),
vol.Required(CONF_METER_TYPE): selector.selector(
{"select": {"options": METER_TYPES}}
),
vol.Required(CONF_METER_OFFSET, default=0): selector.selector(
{
"number": {
"min": 0,
"max": 28,
"mode": "box",
CONF_UNIT_OF_MEASUREMENT: "days",
}
}
),
vol.Optional(CONF_TARIFFS): selector.selector({"text": {}}),
vol.Required(CONF_METER_NET_CONSUMPTION, default=False): selector.selector(
{"boolean": {}}
),
vol.Required(CONF_METER_DELTA_VALUES, default=False): selector.selector(
{"boolean": {}}
),
}
)
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
"user": HelperFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_config)
}
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
"init": HelperFlowFormStep(OPTIONS_SCHEMA)
}
class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
"""Handle a config or options flow for Utility Meter."""
config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title."""
return cast(str, options[CONF_NAME])

View File

@ -1,10 +1,18 @@
{
"domain": "utility_meter",
"integration_type": "helper",
"name": "Utility Meter",
"documentation": "https://www.home-assistant.io/integrations/utility_meter",
"requirements": ["croniter==1.0.6"],
"codeowners": ["@dgomes"],
"requirements": [
"croniter==1.0.6"
],
"codeowners": [
"@dgomes"
],
"quality_scale": "internal",
"iot_class": "local_push",
"loggers": ["croniter"]
"loggers": [
"croniter"
],
"config_flow": true
}

View File

@ -12,11 +12,12 @@ from homeassistant.components.select.const import (
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE
from homeassistant.core import Event, callback, split_entity_id
from homeassistant.core import Event, HomeAssistant, callback, split_entity_id
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.restore_state import RestoreEntity
@ -26,17 +27,34 @@ from .const import (
CONF_METER,
CONF_TARIFFS,
DATA_LEGACY_COMPONENT,
DOMAIN,
SERVICE_RESET,
SERVICE_SELECT_NEXT_TARIFF,
SERVICE_SELECT_TARIFF,
SIGNAL_RESET_METER,
TARIFF_ICON,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize Utility Meter config entry."""
name = config_entry.title
# Remove when frontend list selector is available
if not config_entry.options.get(CONF_TARIFFS):
tariffs = []
else:
tariffs = config_entry.options[CONF_TARIFFS].split(",")
legacy_add_entities = None
unique_id = config_entry.entry_id
tariff_select = TariffSelect(name, tariffs, legacy_add_entities, unique_id)
async_add_entities([tariff_select])
async def async_setup_platform(hass, conf, async_add_entities, discovery_info=None):
"""Set up the utility meter select."""
legacy_component = hass.data[DATA_LEGACY_COMPONENT]
@ -46,35 +64,11 @@ async def async_setup_platform(hass, conf, async_add_entities, discovery_info=No
discovery_info[CONF_METER],
discovery_info[CONF_TARIFFS],
legacy_component.async_add_entities,
None,
)
]
)
async def async_reset_meters(service_call):
"""Reset all sensors of a meter."""
entity_id = service_call.data["entity_id"]
domain = split_entity_id(entity_id)[0]
if domain == DOMAIN:
for entity in legacy_component.entities:
if entity_id == entity.entity_id:
_LOGGER.debug(
"forward reset meter from %s to %s",
entity_id,
entity.tracked_entity_id,
)
entity_id = entity.tracked_entity_id
_LOGGER.debug("reset meter %s", entity_id)
async_dispatcher_send(hass, SIGNAL_RESET_METER, entity_id)
hass.services.async_register(
DOMAIN,
SERVICE_RESET,
async_reset_meters,
vol.Schema({ATTR_ENTITY_ID: cv.entity_id}),
)
legacy_component.async_register_entity_service(
SERVICE_SELECT_TARIFF,
{vol.Required(ATTR_TARIFF): cv.string},
@ -89,9 +83,10 @@ async def async_setup_platform(hass, conf, async_add_entities, discovery_info=No
class TariffSelect(SelectEntity, RestoreEntity):
"""Representation of a Tariff selector."""
def __init__(self, name, tariffs, add_legacy_entities):
def __init__(self, name, tariffs, add_legacy_entities, unique_id):
"""Initialize a tariff selector."""
self._attr_name = name
self._attr_unique_id = unique_id
self._current_tariff = None
self._tariffs = tariffs
self._attr_icon = TARIFF_ICON
@ -112,7 +107,8 @@ class TariffSelect(SelectEntity, RestoreEntity):
"""Run when entity about to be added."""
await super().async_added_to_hass()
await self._add_legacy_entities([LegacyTariffSelect(self.entity_id)])
if self._add_legacy_entities:
await self._add_legacy_entities([LegacyTariffSelect(self.entity_id)])
state = await self.async_get_last_state()
if not state or state.state not in self._tariffs:

View File

@ -1,7 +1,7 @@
"""Utility meter from sensors providing raw data."""
from __future__ import annotations
from datetime import datetime
from datetime import datetime, timedelta
from decimal import Decimal, DecimalException, InvalidOperation
import logging
@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
@ -24,7 +25,7 @@ from homeassistant.const import (
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_platform
from homeassistant.helpers import entity_platform, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import (
@ -49,6 +50,7 @@ from .const import (
CONF_SOURCE_SENSOR,
CONF_TARIFF,
CONF_TARIFF_ENTITY,
CONF_TARIFFS,
DAILY,
DATA_TARIFF_SENSORS,
DATA_UTILITY,
@ -93,6 +95,84 @@ PAUSED = "paused"
COLLECTING = "collecting"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize Utility Meter config entry."""
entry_id = config_entry.entry_id
registry = er.async_get(hass)
# Validate + resolve entity registry id to entity_id
source_entity_id = er.async_validate_entity_id(
registry, config_entry.options[CONF_SOURCE_SENSOR]
)
cron_pattern = None
delta_values = config_entry.options[CONF_METER_DELTA_VALUES]
meter_offset = timedelta(days=config_entry.options[CONF_METER_OFFSET])
meter_type = config_entry.options[CONF_METER_TYPE]
if meter_type == "none":
meter_type = None
name = config_entry.title
net_consumption = config_entry.options[CONF_METER_NET_CONSUMPTION]
tariff_entity = hass.data[DATA_UTILITY][entry_id][CONF_TARIFF_ENTITY]
meters = []
# Remove when frontend list selector is available
if not config_entry.options.get(CONF_TARIFFS):
tariffs = []
else:
tariffs = config_entry.options[CONF_TARIFFS].split(",")
if not tariffs:
# Add single sensor, not gated by a tariff selector
meter_sensor = UtilityMeterSensor(
cron_pattern=cron_pattern,
delta_values=delta_values,
meter_offset=meter_offset,
meter_type=meter_type,
name=name,
net_consumption=net_consumption,
parent_meter=entry_id,
source_entity=source_entity_id,
tariff_entity=tariff_entity,
tariff=None,
unique_id=entry_id,
)
meters.append(meter_sensor)
hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append(meter_sensor)
else:
# Add sensors for each tariff
for tariff in tariffs:
meter_sensor = UtilityMeterSensor(
cron_pattern=cron_pattern,
delta_values=delta_values,
meter_offset=meter_offset,
meter_type=meter_type,
name=f"{name} {tariff}",
net_consumption=net_consumption,
parent_meter=entry_id,
source_entity=source_entity_id,
tariff_entity=tariff_entity,
tariff=tariff,
unique_id=f"{entry_id}_{tariff}",
)
meters.append(meter_sensor)
hass.data[DATA_UTILITY][entry_id][DATA_TARIFF_SENSORS].append(meter_sensor)
async_add_entities(meters)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_CALIBRATE_METER,
{vol.Required(ATTR_VALUE): vol.Coerce(Decimal)},
"async_calibrate",
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
@ -121,16 +201,17 @@ async def async_setup_platform(
)
conf_cron_pattern = hass.data[DATA_UTILITY][meter].get(CONF_CRON_PATTERN)
meter_sensor = UtilityMeterSensor(
meter,
conf_meter_source,
conf.get(CONF_NAME),
conf_meter_type,
conf_meter_offset,
conf_meter_delta_values,
conf_meter_net_consumption,
conf.get(CONF_TARIFF),
conf_meter_tariff_entity,
conf_cron_pattern,
cron_pattern=conf_cron_pattern,
delta_values=conf_meter_delta_values,
meter_offset=conf_meter_offset,
meter_type=conf_meter_type,
name=conf.get(CONF_NAME),
net_consumption=conf_meter_net_consumption,
parent_meter=meter,
source_entity=conf_meter_source,
tariff_entity=conf_meter_tariff_entity,
tariff=conf.get(CONF_TARIFF),
unique_id=None,
)
meters.append(meter_sensor)
@ -152,18 +233,21 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity):
def __init__(
self,
*,
cron_pattern,
delta_values,
meter_offset,
meter_type,
name,
net_consumption,
parent_meter,
source_entity,
name,
meter_type,
meter_offset,
delta_values,
net_consumption,
tariff=None,
tariff_entity=None,
cron_pattern=None,
tariff_entity,
tariff,
unique_id,
):
"""Initialize the Utility Meter sensor."""
self._attr_unique_id = unique_id
self._parent_meter = parent_meter
self._sensor_source_id = source_entity
self._state = None

View File

@ -0,0 +1,28 @@
{
"config": {
"step": {
"user": {
"title": "New Utility Meter",
"description": "The utility meter sensor provides functionality to track consumptions of various utilities (e.g., energy, gas, water, heating) over a configured period of time, typically monthly. The utility meter sensor also supports splitting the consumption by tariffs.\nMeter reset offset allows offsetting the day of monthly meter reset.\nSupported tariffs is a comma separated list of supported tariffs, leave empty if only a single tariff is needed.",
"data": {
"cycle": "Meter reset cycle",
"delta_values": "Delta values",
"name": "Name",
"net_consumption": "Net consumption",
"offset": "Meter reset offset",
"source": "Input sensor",
"tariffs": "Supported tariffs"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"source": "[%key:component::utility_meter::config::step::user::data::source%]"
}
}
}
}
}

View File

@ -0,0 +1,28 @@
{
"config": {
"step": {
"user": {
"data": {
"cycle": "Meter reset cycle",
"delta_values": "Delta values",
"name": "Name",
"net_consumption": "Net consumption",
"offset": "Meter reset offset",
"source": "Input sensor",
"tariffs": "Supported tariffs"
},
"description": "The utility meter sensor provides functionality to track consumptions of various utilities (e.g., energy, gas, water, heating) over a configured period of time, typically monthly. The utility meter sensor also supports splitting the consumption by tariffs.\nMeter reset offset allows offsetting the day of monthly meter reset.\nSupported tariffs is a comma separated list of supported tariffs, leave empty if only a single tariff is needed.",
"title": "New Utility Meter"
}
}
},
"options": {
"step": {
"init": {
"data": {
"source": "Input sensor"
}
}
}
}
}

View File

@ -405,6 +405,7 @@ FLOWS = {
"min_max",
"switch_as_x",
"threshold",
"tod"
"tod",
"utility_meter"
]
}

View File

@ -0,0 +1,209 @@
"""Test the Utility Meter config flow."""
from unittest.mock import patch
import pytest
from homeassistant import config_entries
from homeassistant.components.utility_meter.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
from tests.common import MockConfigEntry
@pytest.mark.parametrize("platform", ("sensor",))
async def test_config_flow(hass: HomeAssistant, platform) -> None:
"""Test the config flow."""
input_sensor_entity_id = "sensor.input"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] is None
with patch(
"homeassistant.components.utility_meter.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"cycle": "monthly",
"name": "Electricity meter",
"offset": 0,
"source": input_sensor_entity_id,
"tariffs": "",
},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Electricity meter"
assert result["data"] == {}
assert result["options"] == {
"cycle": "monthly",
"delta_values": False,
"name": "Electricity meter",
"net_consumption": False,
"offset": 0,
"source": input_sensor_entity_id,
"tariffs": "",
}
assert len(mock_setup_entry.mock_calls) == 1
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
assert config_entry.data == {}
assert config_entry.options == {
"cycle": "monthly",
"delta_values": False,
"name": "Electricity meter",
"net_consumption": False,
"offset": 0,
"source": input_sensor_entity_id,
"tariffs": "",
}
assert config_entry.title == "Electricity meter"
async def test_tariffs(hass: HomeAssistant) -> None:
"""Test tariffs."""
input_sensor_entity_id = "sensor.input"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"cycle": "monthly",
"name": "Electricity meter",
"offset": 0,
"source": input_sensor_entity_id,
"tariffs": "cat,dog,horse,cow",
},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Electricity meter"
assert result["data"] == {}
assert result["options"] == {
"cycle": "monthly",
"delta_values": False,
"name": "Electricity meter",
"net_consumption": False,
"offset": 0,
"source": input_sensor_entity_id,
"tariffs": "cat,dog,horse,cow",
}
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
assert config_entry.data == {}
assert config_entry.options == {
"cycle": "monthly",
"delta_values": False,
"name": "Electricity meter",
"net_consumption": False,
"offset": 0,
"source": input_sensor_entity_id,
"tariffs": "cat,dog,horse,cow",
}
assert config_entry.title == "Electricity meter"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"cycle": "monthly",
"name": "Electricity meter",
"offset": 0,
"source": input_sensor_entity_id,
"tariffs": "cat,cat,cat,cat",
},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"]["base"] == "tariffs_not_unique"
def get_suggested(schema, key):
"""Get suggested value for key in voluptuous schema."""
for k in schema.keys():
if k == key:
if k.description is None or "suggested_value" not in k.description:
return None
return k.description["suggested_value"]
# Wanted key absent from schema
raise Exception
async def test_options(hass: HomeAssistant) -> None:
"""Test reconfiguring."""
input_sensor1_entity_id = "sensor.input1"
input_sensor2_entity_id = "sensor.input2"
# Setup the config entry
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={
"cycle": "monthly",
"delta_values": False,
"name": "Electricity meter",
"net_consumption": False,
"offset": 0,
"source": input_sensor1_entity_id,
"tariffs": "",
},
title="Electricity meter",
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "init"
schema = result["data_schema"].schema
assert get_suggested(schema, "source") == input_sensor1_entity_id
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"source": input_sensor2_entity_id},
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["data"] == {
"cycle": "monthly",
"delta_values": False,
"name": "Electricity meter",
"net_consumption": False,
"offset": 0,
"source": input_sensor2_entity_id,
"tariffs": "",
}
assert config_entry.data == {}
assert config_entry.options == {
"cycle": "monthly",
"delta_values": False,
"name": "Electricity meter",
"net_consumption": False,
"offset": 0,
"source": input_sensor2_entity_id,
"tariffs": "",
}
assert config_entry.title == "Electricity meter"
# Check config entry is reloaded with new options
await hass.async_block_till_done()
state = hass.states.get("sensor.electricity_meter")
assert state.attributes["source"] == input_sensor2_entity_id

View File

@ -1,7 +1,11 @@
"""The tests for the utility_meter component."""
from __future__ import annotations
from datetime import timedelta
from unittest.mock import patch
import pytest
from homeassistant.components.select.const import (
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
@ -22,12 +26,14 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
Platform,
)
from homeassistant.core import State
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import ServiceNotFound
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import mock_restore_cache
from tests.common import MockConfigEntry, mock_restore_cache
async def test_restore_state(hass):
@ -168,8 +174,138 @@ async def test_services(hass):
assert state.state == "4"
async def test_services_config_entry(hass):
"""Test energy sensor reset service."""
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={
"cycle": "monthly",
"delta_values": False,
"name": "Energy bill",
"net_consumption": False,
"offset": 0,
"source": "sensor.energy",
"tariffs": "peak,offpeak",
},
title="Energy bill",
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={
"cycle": "monthly",
"delta_values": False,
"name": "Energy bill2",
"net_consumption": False,
"offset": 0,
"source": "sensor.energy",
"tariffs": "peak,offpeak",
},
title="Energy bill2",
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
entity_id = "sensor.energy"
hass.states.async_set(
entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
)
await hass.async_block_till_done()
now = dt_util.utcnow() + timedelta(seconds=10)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.states.async_set(
entity_id,
3,
{ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_bill_peak")
assert state.state == "2"
state = hass.states.get("sensor.energy_bill_offpeak")
assert state.state == "0"
# Next tariff - only supported on legacy entity
with pytest.raises(ServiceNotFound):
data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"}
await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data)
await hass.async_block_till_done()
# Change tariff
data = {ATTR_ENTITY_ID: "select.energy_bill", "option": "offpeak"}
await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data)
await hass.async_block_till_done()
now += timedelta(seconds=10)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.states.async_set(
entity_id,
4,
{ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_bill_peak")
assert state.state == "2"
state = hass.states.get("sensor.energy_bill_offpeak")
assert state.state == "1"
# Change tariff
data = {ATTR_ENTITY_ID: "select.energy_bill", "option": "wrong_tariff"}
await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data)
await hass.async_block_till_done()
# Inexisting tariff, ignoring
assert hass.states.get("select.energy_bill").state != "wrong_tariff"
data = {ATTR_ENTITY_ID: "select.energy_bill", "option": "peak"}
await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data)
await hass.async_block_till_done()
now += timedelta(seconds=10)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.states.async_set(
entity_id,
5,
{ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
force_update=True,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_bill_peak")
assert state.state == "3"
state = hass.states.get("sensor.energy_bill_offpeak")
assert state.state == "1"
# Reset meters
data = {ATTR_ENTITY_ID: "select.energy_bill"}
await hass.services.async_call(DOMAIN, SERVICE_RESET, data)
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_bill_peak")
assert state.state == "0"
state = hass.states.get("sensor.energy_bill_offpeak")
assert state.state == "0"
# meanwhile energy_bill2_peak accumulated all kWh
state = hass.states.get("sensor.energy_bill2_peak")
assert state.state == "4"
async def test_cron(hass, legacy_patchable_time):
"""Test cron pattern and offset fails."""
"""Test cron pattern."""
config = {
"utility_meter": {
@ -327,3 +463,61 @@ async def test_legacy_support(hass):
await hass.services.async_call(DOMAIN, SERVICE_RESET, data)
await hass.async_block_till_done()
assert reset_calls == ["select.energy_bill"]
@pytest.mark.parametrize(
"tariffs,expected_entities",
(
(
"",
["sensor.electricity_meter"],
),
(
"high,low",
[
"sensor.electricity_meter_low",
"sensor.electricity_meter_high",
"select.electricity_meter",
],
),
),
)
async def test_setup_and_remove_config_entry(
hass: HomeAssistant, tariffs: str, expected_entities: list[str]
) -> None:
"""Test setting up and removing a config entry."""
input_sensor_entity_id = "sensor.input"
registry = er.async_get(hass)
# Setup the config entry
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={
"cycle": "monthly",
"delta_values": False,
"name": "Electricity meter",
"net_consumption": False,
"offset": 0,
"source": input_sensor_entity_id,
"tariffs": tariffs,
},
title="Electricity meter",
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == len(expected_entities)
assert len(registry.entities) == len(expected_entities)
for entity in expected_entities:
assert hass.states.get(entity)
assert entity in registry.entities
# Remove the config entry
assert await hass.config_entries.async_remove(config_entry.entry_id)
await hass.async_block_till_done()
# Check the state and entity registry entry are removed
assert len(hass.states.async_all()) == 0
assert len(registry.entities) == 0

View File

@ -3,6 +3,8 @@ from contextlib import contextmanager
from datetime import timedelta
from unittest.mock import patch
import pytest
from homeassistant.components.select.const import (
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
@ -39,7 +41,7 @@ from homeassistant.core import State
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed, mock_restore_cache
from tests.common import MockConfigEntry, async_fire_time_changed, mock_restore_cache
@contextmanager
@ -52,24 +54,55 @@ def alter_time(retval):
yield
async def test_state(hass):
"""Test utility sensor state."""
config = {
"utility_meter": {
"energy_bill": {
@pytest.mark.parametrize(
"yaml_config,config_entry_config",
(
(
{
"utility_meter": {
"energy_bill": {
"source": "sensor.energy",
"tariffs": ["onpeak", "midpeak", "offpeak"],
}
}
},
None,
),
(
None,
{
"cycle": "none",
"delta_values": False,
"name": "Energy bill",
"net_consumption": False,
"offset": 0,
"source": "sensor.energy",
"tariffs": ["onpeak", "midpeak", "offpeak"],
}
}
}
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
"tariffs": "onpeak,midpeak,offpeak",
},
),
),
)
async def test_state(hass, yaml_config, config_entry_config):
"""Test utility sensor state."""
if yaml_config:
assert await async_setup_component(hass, DOMAIN, yaml_config)
await hass.async_block_till_done()
entity_id = yaml_config[DOMAIN]["energy_bill"]["source"]
else:
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options=config_entry_config,
title=config_entry_config["name"],
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
entity_id = config_entry_config["source"]
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
entity_id = config[DOMAIN]["energy_bill"]["source"]
hass.states.async_set(
entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
)
@ -175,7 +208,6 @@ async def test_state(hass):
assert state.state == "0.123"
# test invalid state
entity_id = config[DOMAIN]["energy_bill"]["source"]
hass.states.async_set(
entity_id, "*", {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
)
@ -185,7 +217,6 @@ async def test_state(hass):
assert state.state == "0.123"
# test unavailable source
entity_id = config[DOMAIN]["energy_bill"]["source"]
hass.states.async_set(
entity_id, STATE_UNAVAILABLE, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
)
@ -195,19 +226,51 @@ async def test_state(hass):
assert state.state == "0.123"
async def test_init(hass):
"""Test utility sensor state initializtion."""
config = {
"utility_meter": {
"energy_bill": {
@pytest.mark.parametrize(
"yaml_config,config_entry_config",
(
(
{
"utility_meter": {
"energy_bill": {
"source": "sensor.energy",
"tariffs": ["onpeak", "midpeak", "offpeak"],
}
}
},
None,
),
(
None,
{
"cycle": "none",
"delta_values": False,
"name": "Energy bill",
"net_consumption": False,
"offset": 0,
"source": "sensor.energy",
"tariffs": ["onpeak", "midpeak", "offpeak"],
}
}
}
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
"tariffs": "onpeak,midpeak,offpeak",
},
),
),
)
async def test_init(hass, yaml_config, config_entry_config):
"""Test utility sensor state initializtion."""
if yaml_config:
assert await async_setup_component(hass, DOMAIN, yaml_config)
await hass.async_block_till_done()
entity_id = yaml_config[DOMAIN]["energy_bill"]["source"]
else:
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options=config_entry_config,
title=config_entry_config["name"],
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
entity_id = config_entry_config["source"]
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
@ -220,7 +283,6 @@ async def test_init(hass):
assert state is not None
assert state.state == STATE_UNKNOWN
entity_id = config[DOMAIN]["energy_bill"]["source"]
hass.states.async_set(
entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
)
@ -238,31 +300,74 @@ async def test_init(hass):
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
async def test_device_class(hass):
@pytest.mark.parametrize(
"yaml_config,config_entry_configs",
(
(
{
"utility_meter": {
"energy_meter": {
"source": "sensor.energy",
"net_consumption": True,
},
"gas_meter": {
"source": "sensor.gas",
},
}
},
None,
),
(
None,
[
{
"cycle": "none",
"delta_values": False,
"name": "Energy meter",
"net_consumption": True,
"offset": 0,
"source": "sensor.energy",
"tariffs": "",
},
{
"cycle": "none",
"delta_values": False,
"name": "Gas meter",
"net_consumption": False,
"offset": 0,
"source": "sensor.gas",
"tariffs": "",
},
],
),
),
)
async def test_device_class(hass, yaml_config, config_entry_configs):
"""Test utility device_class."""
config = {
"utility_meter": {
"energy_meter": {
"source": "sensor.energy",
"net_consumption": True,
},
"gas_meter": {
"source": "sensor.gas",
},
}
}
if yaml_config:
assert await async_setup_component(hass, DOMAIN, yaml_config)
await hass.async_block_till_done()
else:
for config_entry_config in config_entry_configs:
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options=config_entry_config,
title=config_entry_config["name"],
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
entity_id_energy = "sensor.energy"
entity_id_gas = "sensor.gas"
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
entity_id_energy = config[DOMAIN]["energy_meter"]["source"]
hass.states.async_set(
entity_id_energy, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
)
entity_id_gas = config[DOMAIN]["gas_meter"]["source"]
hass.states.async_set(
entity_id_gas, 2, {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"}
)
@ -283,17 +388,37 @@ async def test_device_class(hass):
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "some_archaic_unit"
async def test_restore_state(hass):
@pytest.mark.parametrize(
"yaml_config,config_entry_config",
(
(
{
"utility_meter": {
"energy_bill": {
"source": "sensor.energy",
"tariffs": ["onpeak", "midpeak", "offpeak"],
}
}
},
None,
),
(
None,
{
"cycle": "none",
"delta_values": False,
"name": "Energy bill",
"net_consumption": False,
"offset": 0,
"source": "sensor.energy",
"tariffs": "onpeak,midpeak,offpeak",
},
),
),
)
async def test_restore_state(hass, yaml_config, config_entry_config):
"""Test utility sensor restore state."""
last_reset = "2020-12-21T00:00:00.013073+00:00"
config = {
"utility_meter": {
"energy_bill": {
"source": "sensor.energy",
"tariffs": ["onpeak", "midpeak", "offpeak"],
}
}
}
mock_restore_cache(
hass,
[
@ -322,8 +447,19 @@ async def test_restore_state(hass):
],
)
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
if yaml_config:
assert await async_setup_component(hass, DOMAIN, yaml_config)
await hass.async_block_till_done()
else:
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options=config_entry_config,
title=config_entry_config["name"],
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# restore from cache
state = hass.states.get("sensor.energy_bill_onpeak")
@ -355,19 +491,53 @@ async def test_restore_state(hass):
assert state.attributes.get("status") == PAUSED
async def test_net_consumption(hass):
@pytest.mark.parametrize(
"yaml_config,config_entry_config",
(
(
{
"utility_meter": {
"energy_bill": {
"net_consumption": True,
"source": "sensor.energy",
}
}
},
None,
),
(
None,
{
"cycle": "none",
"delta_values": False,
"name": "Energy bill",
"net_consumption": True,
"offset": 0,
"source": "sensor.energy",
"tariffs": "",
},
),
),
)
async def test_net_consumption(hass, yaml_config, config_entry_config):
"""Test utility sensor state."""
config = {
"utility_meter": {
"energy_bill": {"source": "sensor.energy", "net_consumption": True}
}
}
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
if yaml_config:
assert await async_setup_component(hass, DOMAIN, yaml_config)
await hass.async_block_till_done()
entity_id = yaml_config[DOMAIN]["energy_bill"]["source"]
else:
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options=config_entry_config,
title=config_entry_config["name"],
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
entity_id = config_entry_config["source"]
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
entity_id = config[DOMAIN]["energy_bill"]["source"]
hass.states.async_set(
entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
)
@ -389,19 +559,53 @@ async def test_net_consumption(hass):
assert state.state == "-1"
async def test_non_net_consumption(hass):
@pytest.mark.parametrize(
"yaml_config,config_entry_config",
(
(
{
"utility_meter": {
"energy_bill": {
"net_consumption": False,
"source": "sensor.energy",
}
}
},
None,
),
(
None,
{
"cycle": "none",
"delta_values": False,
"name": "Energy bill",
"net_consumption": False,
"offset": 0,
"source": "sensor.energy",
"tariffs": "",
},
),
),
)
async def test_non_net_consumption(hass, yaml_config, config_entry_config):
"""Test utility sensor state."""
config = {
"utility_meter": {
"energy_bill": {"source": "sensor.energy", "net_consumption": False}
}
}
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
if yaml_config:
assert await async_setup_component(hass, DOMAIN, yaml_config)
await hass.async_block_till_done()
entity_id = yaml_config[DOMAIN]["energy_bill"]["source"]
else:
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options=config_entry_config,
title=config_entry_config["name"],
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
entity_id = config_entry_config["source"]
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
entity_id = config[DOMAIN]["energy_bill"]["source"]
hass.states.async_set(
entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}
)
@ -423,21 +627,55 @@ async def test_non_net_consumption(hass):
assert state.state == "0"
async def test_delta_values(hass):
@pytest.mark.parametrize(
"yaml_config,config_entry_config",
(
(
{
"utility_meter": {
"energy_bill": {
"delta_values": True,
"source": "sensor.energy",
}
}
},
None,
),
(
None,
{
"cycle": "none",
"delta_values": True,
"name": "Energy bill",
"net_consumption": False,
"offset": 0,
"source": "sensor.energy",
"tariffs": "",
},
),
),
)
async def test_delta_values(hass, yaml_config, config_entry_config):
"""Test utility meter "delta_values" mode."""
config = {
"utility_meter": {
"energy_bill": {"source": "sensor.energy", "delta_values": True}
}
}
now = dt_util.utcnow()
with alter_time(now):
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
if yaml_config:
assert await async_setup_component(hass, DOMAIN, yaml_config)
await hass.async_block_till_done()
entity_id = yaml_config[DOMAIN]["energy_bill"]["source"]
else:
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options=config_entry_config,
title=config_entry_config["name"],
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
entity_id = config_entry_config["source"]
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
entity_id = config[DOMAIN]["energy_bill"]["source"]
async_fire_time_changed(hass, now)
hass.states.async_set(