1 """Support for tracking consumption over given periods of time."""
3 from datetime
import datetime, timedelta
6 from cronsim
import CronSim, CronSimError
7 import voluptuous
as vol
17 async_remove_stale_devices_links_keep_entity_device,
25 CONF_METER_DELTA_VALUES,
26 CONF_METER_NET_CONSUMPTION,
28 CONF_METER_PERIODICALLY_RESETTING,
30 CONF_SENSOR_ALWAYS_AVAILABLE,
43 _LOGGER = logging.getLogger(__name__)
49 """Check that the pattern is well-formed."""
51 CronSim(pattern,
datetime(2020, 1, 1))
52 except CronSimError
as err:
53 _LOGGER.error(
"Invalid cron pattern %s: %s", pattern, err)
54 raise vol.Invalid(
"Invalid pattern")
from err
59 """Check that if cron pattern is used, then meter type and offsite must be removed."""
60 if CONF_CRON_PATTERN
in config
and CONF_METER_TYPE
in config:
61 raise vol.Invalid(f
"Use <{CONF_CRON_PATTERN}> or <{CONF_METER_TYPE}>")
63 CONF_CRON_PATTERN
in config
64 and CONF_METER_OFFSET
in config
65 and config[CONF_METER_OFFSET] != DEFAULT_OFFSET
68 f
"When <{CONF_CRON_PATTERN}> is used <{CONF_METER_OFFSET}> has no meaning"
74 """Check that time period does not include more than 28 days."""
77 "Unsupported offset of more than 28 days, please use a cron pattern."
83 METER_CONFIG_SCHEMA = vol.Schema(
86 vol.Required(CONF_SOURCE_SENSOR): cv.entity_id,
87 vol.Optional(CONF_NAME): cv.string,
88 vol.Optional(CONF_UNIQUE_ID): cv.string,
89 vol.Optional(CONF_METER_TYPE): vol.In(METER_TYPES),
90 vol.Optional(CONF_METER_OFFSET, default=DEFAULT_OFFSET): vol.All(
91 cv.time_period, cv.positive_timedelta, max_28_days
93 vol.Optional(CONF_METER_DELTA_VALUES, default=
False): cv.boolean,
94 vol.Optional(CONF_METER_NET_CONSUMPTION, default=
False): cv.boolean,
95 vol.Optional(CONF_METER_PERIODICALLY_RESETTING, default=
True): cv.boolean,
96 vol.Optional(CONF_TARIFFS, default=[]): vol.All(
97 cv.ensure_list, vol.Unique(), [cv.string]
99 vol.Optional(CONF_CRON_PATTERN): validate_cron_pattern,
100 vol.Optional(CONF_SENSOR_ALWAYS_AVAILABLE, default=
False): cv.boolean,
106 CONFIG_SCHEMA = vol.Schema(
107 {DOMAIN: vol.Schema({cv.slug: METER_CONFIG_SCHEMA})}, extra=vol.ALLOW_EXTRA
111 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
112 """Set up an Utility Meter."""
113 hass.data[DATA_UTILITY] = {}
115 async
def async_reset_meters(service_call):
116 """Reset all sensors of a meter."""
117 meters = service_call.data[
"entity_id"]
120 _LOGGER.debug(
"resetting meter %s", meter)
125 hass, SIGNAL_RESET_METER, f
"{SELECT_DOMAIN}.{entity}"
130 hass.services.async_register(
134 vol.Schema({ATTR_ENTITY_ID: vol.All(cv.ensure_list, [cv.entity_id])}),
137 if DOMAIN
not in config:
140 for meter, conf
in config[DOMAIN].items():
141 _LOGGER.debug(
"Setup %s.%s", DOMAIN, meter)
143 hass.data[DATA_UTILITY][meter] = conf
144 hass.data[DATA_UTILITY][meter][DATA_TARIFF_SENSORS] = []
146 if not conf[CONF_TARIFFS]:
148 hass.async_create_task(
149 discovery.async_load_platform(
153 {meter: {CONF_METER: meter}},
160 hass.async_create_task(
161 discovery.async_load_platform(
165 {CONF_METER: meter, CONF_TARIFFS: conf[CONF_TARIFFS]},
171 hass.data[DATA_UTILITY][meter][CONF_TARIFF_ENTITY] = (
172 f
"{SELECT_DOMAIN}.{meter}"
177 for tariff
in conf[CONF_TARIFFS]:
178 name = f
"{meter} {tariff}"
179 tariff_confs[name] = {
184 hass.async_create_task(
185 discovery.async_load_platform(
186 hass, SENSOR_DOMAIN, DOMAIN, tariff_confs, config
195 """Set up Utility Meter from a config entry."""
198 hass, entry.entry_id, entry.options[CONF_SOURCE_SENSOR]
201 entity_registry = er.async_get(hass)
202 hass.data[DATA_UTILITY][entry.entry_id] = {
203 "source": entry.options[CONF_SOURCE_SENSOR],
205 hass.data[DATA_UTILITY][entry.entry_id][DATA_TARIFF_SENSORS] = []
208 er.async_validate_entity_id(entity_registry, entry.options[CONF_SOURCE_SENSOR])
212 "Failed to setup utility_meter for unknown entity %s",
213 entry.options[CONF_SOURCE_SENSOR],
217 if not entry.options.get(CONF_TARIFFS):
219 hass.data[DATA_UTILITY][entry.entry_id][CONF_TARIFF_ENTITY] =
None
220 await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
223 entity_entry = entity_registry.async_get_or_create(
224 Platform.SELECT, DOMAIN, entry.entry_id, suggested_object_id=entry.title
226 hass.data[DATA_UTILITY][entry.entry_id][CONF_TARIFF_ENTITY] = (
227 entity_entry.entity_id
229 await hass.config_entries.async_forward_entry_setups(
230 entry, (Platform.SELECT, Platform.SENSOR)
233 entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
239 """Update listener, called when the config entry options are changed."""
241 await hass.config_entries.async_reload(entry.entry_id)
245 """Unload a config entry."""
246 platforms_to_unload = [Platform.SENSOR]
247 if entry.options.get(CONF_TARIFFS):
248 platforms_to_unload.append(Platform.SELECT)
250 if unload_ok := await hass.config_entries.async_unload_platforms(
254 hass.data[DATA_UTILITY].pop(entry.entry_id)
260 """Migrate old entry."""
261 _LOGGER.debug(
"Migrating from version %s", config_entry.version)
263 if config_entry.version == 1:
264 new = {**config_entry.options}
265 new[CONF_METER_PERIODICALLY_RESETTING] =
True
266 hass.config_entries.async_update_entry(config_entry, options=new, version=2)
268 _LOGGER.info(
"Migration to version %s successful", config_entry.version)
def period_or_cron(config)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
bool async_migrate_entry(HomeAssistant hass, ConfigEntry config_entry)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
def validate_cron_pattern(pattern)
bool async_setup(HomeAssistant hass, ConfigType config)
None config_entry_update_listener(HomeAssistant hass, ConfigEntry entry)
tuple[str, str] split_entity_id(str entity_id)
datetime_sys datetime(Any value)
None async_remove_stale_devices_links_keep_entity_device(HomeAssistant hass, str entry_id, str source_entity_id_or_uuid)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)