Home Assistant Unofficial Reference 2024.12.1
data.py
Go to the documentation of this file.
1 """Energy data."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections import Counter
7 from collections.abc import Awaitable, Callable
8 from typing import Literal, TypedDict
9 
10 import voluptuous as vol
11 
12 from homeassistant.core import HomeAssistant, callback
13 from homeassistant.helpers import config_validation as cv, singleton, storage
14 
15 from .const import DOMAIN
16 
17 STORAGE_VERSION = 1
18 STORAGE_KEY = DOMAIN
19 
20 
21 @singleton.singleton(f"{DOMAIN}_manager")
22 async def async_get_manager(hass: HomeAssistant) -> EnergyManager:
23  """Return an initialized data manager."""
24  manager = EnergyManager(hass)
25  await manager.async_initialize()
26  return manager
27 
28 
29 class FlowFromGridSourceType(TypedDict):
30  """Dictionary describing the 'from' stat for the grid source."""
31 
32  # statistic_id of a an energy meter (kWh)
33  stat_energy_from: str
34 
35  # statistic_id of costs ($) incurred from the energy meter
36  # If set to None and entity_energy_price or number_energy_price are configured,
37  # an EnergyCostSensor will be automatically created
38  stat_cost: str | None
39 
40  # Used to generate costs if stat_cost is set to None
41  entity_energy_price: str | None # entity_id of an entity providing price ($/kWh)
42  number_energy_price: float | None # Price for energy ($/kWh)
43 
44 
45 class FlowToGridSourceType(TypedDict):
46  """Dictionary describing the 'to' stat for the grid source."""
47 
48  # kWh meter
49  stat_energy_to: str
50 
51  # statistic_id of compensation ($) received for contributing back
52  # If set to None and entity_energy_price or number_energy_price are configured,
53  # an EnergyCostSensor will be automatically created
54  stat_compensation: str | None
55 
56  # Used to generate costs if stat_compensation is set to None
57  entity_energy_price: str | None # entity_id of an entity providing price ($/kWh)
58  number_energy_price: float | None # Price for energy ($/kWh)
59 
60 
61 class GridSourceType(TypedDict):
62  """Dictionary holding the source of grid energy consumption."""
63 
64  type: Literal["grid"]
65 
66  flow_from: list[FlowFromGridSourceType]
67  flow_to: list[FlowToGridSourceType]
68 
69  cost_adjustment_day: float
70 
71 
72 class SolarSourceType(TypedDict):
73  """Dictionary holding the source of energy production."""
74 
75  type: Literal["solar"]
76 
77  stat_energy_from: str
78  config_entry_solar_forecast: list[str] | None
79 
80 
81 class BatterySourceType(TypedDict):
82  """Dictionary holding the source of battery storage."""
83 
84  type: Literal["battery"]
85 
86  stat_energy_from: str
87  stat_energy_to: str
88 
89 
90 class GasSourceType(TypedDict):
91  """Dictionary holding the source of gas consumption."""
92 
93  type: Literal["gas"]
94 
95  stat_energy_from: str
96 
97  # statistic_id of costs ($) incurred from the gas meter
98  # If set to None and entity_energy_price or number_energy_price are configured,
99  # an EnergyCostSensor will be automatically created
100  stat_cost: str | None
101 
102  # Used to generate costs if stat_cost is set to None
103  entity_energy_price: str | None # entity_id of an entity providing price ($/m³)
104  number_energy_price: float | None # Price for energy ($/m³)
105 
106 
107 class WaterSourceType(TypedDict):
108  """Dictionary holding the source of water consumption."""
109 
110  type: Literal["water"]
111 
112  stat_energy_from: str
113 
114  # statistic_id of costs ($) incurred from the water meter
115  # If set to None and entity_energy_price or number_energy_price are configured,
116  # an EnergyCostSensor will be automatically created
117  stat_cost: str | None
118 
119  # Used to generate costs if stat_cost is set to None
120  entity_energy_price: str | None # entity_id of an entity providing price ($/m³)
121  number_energy_price: float | None # Price for energy ($/m³)
122 
123 
124 type SourceType = (
125  GridSourceType
126  | SolarSourceType
127  | BatterySourceType
128  | GasSourceType
129  | WaterSourceType
130 )
131 
132 
133 class DeviceConsumption(TypedDict):
134  """Dictionary holding the source of individual device consumption."""
135 
136  # This is an ever increasing value
137  stat_consumption: str
138 
139  # An optional custom name for display in energy graphs
140  name: str | None
141 
142 
143 class EnergyPreferences(TypedDict):
144  """Dictionary holding the energy data."""
145 
146  energy_sources: list[SourceType]
147  device_consumption: list[DeviceConsumption]
148 
149 
151  """all types optional."""
152 
153 
155  val: FlowFromGridSourceType,
156 ) -> FlowFromGridSourceType:
157  """Ensure we use a single price source."""
158  if (
159  val["entity_energy_price"] is not None
160  and val["number_energy_price"] is not None
161  ):
162  raise vol.Invalid("Define either an entity or a fixed number for the price")
163 
164  return val
165 
166 
167 FLOW_FROM_GRID_SOURCE_SCHEMA = vol.All(
168  vol.Schema(
169  {
170  vol.Required("stat_energy_from"): str,
171  vol.Optional("stat_cost"): vol.Any(str, None),
172  # entity_energy_from was removed in HA Core 2022.10
173  vol.Remove("entity_energy_from"): vol.Any(str, None),
174  vol.Optional("entity_energy_price"): vol.Any(str, None),
175  vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
176  }
177  ),
178  _flow_from_ensure_single_price,
179 )
180 
181 
182 FLOW_TO_GRID_SOURCE_SCHEMA = vol.Schema(
183  {
184  vol.Required("stat_energy_to"): str,
185  vol.Optional("stat_compensation"): vol.Any(str, None),
186  # entity_energy_to was removed in HA Core 2022.10
187  vol.Remove("entity_energy_to"): vol.Any(str, None),
188  vol.Optional("entity_energy_price"): vol.Any(str, None),
189  vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
190  }
191 )
192 
193 
194 def _generate_unique_value_validator(key: str) -> Callable[[list[dict]], list[dict]]:
195  """Generate a validator that ensures a value is only used once."""
196 
197  def validate_uniqueness(
198  val: list[dict],
199  ) -> list[dict]:
200  """Ensure that the user doesn't add duplicate values."""
201  counts = Counter(flow_from[key] for flow_from in val)
202 
203  for value, count in counts.items():
204  if count > 1:
205  raise vol.Invalid(f"Cannot specify {value} more than once")
206 
207  return val
208 
209  return validate_uniqueness
210 
211 
212 GRID_SOURCE_SCHEMA = vol.Schema(
213  {
214  vol.Required("type"): "grid",
215  vol.Required("flow_from"): vol.All(
216  [FLOW_FROM_GRID_SOURCE_SCHEMA],
217  _generate_unique_value_validator("stat_energy_from"),
218  ),
219  vol.Required("flow_to"): vol.All(
220  [FLOW_TO_GRID_SOURCE_SCHEMA],
221  _generate_unique_value_validator("stat_energy_to"),
222  ),
223  vol.Required("cost_adjustment_day"): vol.Coerce(float),
224  }
225 )
226 SOLAR_SOURCE_SCHEMA = vol.Schema(
227  {
228  vol.Required("type"): "solar",
229  vol.Required("stat_energy_from"): str,
230  vol.Optional("config_entry_solar_forecast"): vol.Any([str], None),
231  }
232 )
233 BATTERY_SOURCE_SCHEMA = vol.Schema(
234  {
235  vol.Required("type"): "battery",
236  vol.Required("stat_energy_from"): str,
237  vol.Required("stat_energy_to"): str,
238  }
239 )
240 GAS_SOURCE_SCHEMA = vol.Schema(
241  {
242  vol.Required("type"): "gas",
243  vol.Required("stat_energy_from"): str,
244  vol.Optional("stat_cost"): vol.Any(str, None),
245  # entity_energy_from was removed in HA Core 2022.10
246  vol.Remove("entity_energy_from"): vol.Any(str, None),
247  vol.Optional("entity_energy_price"): vol.Any(str, None),
248  vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
249  }
250 )
251 WATER_SOURCE_SCHEMA = vol.Schema(
252  {
253  vol.Required("type"): "water",
254  vol.Required("stat_energy_from"): str,
255  vol.Optional("stat_cost"): vol.Any(str, None),
256  vol.Optional("entity_energy_price"): vol.Any(str, None),
257  vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
258  }
259 )
260 
261 
262 def check_type_limits(value: list[SourceType]) -> list[SourceType]:
263  """Validate that we don't have too many of certain types."""
264  types = Counter([val["type"] for val in value])
265 
266  if types.get("grid", 0) > 1:
267  raise vol.Invalid("You cannot have more than 1 grid source")
268 
269  return value
270 
271 
272 ENERGY_SOURCE_SCHEMA = vol.All(
273  vol.Schema(
274  [
275  cv.key_value_schemas(
276  "type",
277  {
278  "grid": GRID_SOURCE_SCHEMA,
279  "solar": SOLAR_SOURCE_SCHEMA,
280  "battery": BATTERY_SOURCE_SCHEMA,
281  "gas": GAS_SOURCE_SCHEMA,
282  "water": WATER_SOURCE_SCHEMA,
283  },
284  )
285  ]
286  ),
287  check_type_limits,
288 )
289 
290 DEVICE_CONSUMPTION_SCHEMA = vol.Schema(
291  {
292  vol.Required("stat_consumption"): str,
293  vol.Optional("name"): str,
294  }
295 )
296 
297 
299  """Manage the instance energy prefs."""
300 
301  def __init__(self, hass: HomeAssistant) -> None:
302  """Initialize energy manager."""
303  self._hass_hass = hass
304  self._store_store = storage.Store[EnergyPreferences](
305  hass, STORAGE_VERSION, STORAGE_KEY
306  )
307  self.datadata: EnergyPreferences | None = None
308  self._update_listeners: list[Callable[[], Awaitable]] = []
309 
310  async def async_initialize(self) -> None:
311  """Initialize the energy integration."""
312  self.datadata = await self._store_store.async_load()
313 
314  @staticmethod
315  def default_preferences() -> EnergyPreferences:
316  """Return default preferences."""
317  return {
318  "energy_sources": [],
319  "device_consumption": [],
320  }
321 
322  async def async_update(self, update: EnergyPreferencesUpdate) -> None:
323  """Update the preferences."""
324  if self.datadata is None:
325  data = EnergyManager.default_preferences()
326  else:
327  data = self.datadata.copy()
328 
329  for key in (
330  "energy_sources",
331  "device_consumption",
332  ):
333  if key in update:
334  data[key] = update[key]
335 
336  self.datadata = data
337  self._store_store.async_delay_save(lambda: data, 60)
338 
339  if not self._update_listeners:
340  return
341 
342  await asyncio.gather(*(listener() for listener in self._update_listeners))
343 
344  @callback
345  def async_listen_updates(self, update_listener: Callable[[], Awaitable]) -> None:
346  """Listen for data updates."""
347  self._update_listeners.append(update_listener)
None async_listen_updates(self, Callable[[], Awaitable] update_listener)
Definition: data.py:345
None __init__(self, HomeAssistant hass)
Definition: data.py:301
None async_update(self, EnergyPreferencesUpdate update)
Definition: data.py:322
Callable[[list[dict]], list[dict]] _generate_unique_value_validator(str key)
Definition: data.py:194
EnergyManager async_get_manager(HomeAssistant hass)
Definition: data.py:22
list[SourceType] check_type_limits(list[SourceType] value)
Definition: data.py:262
FlowFromGridSourceType _flow_from_ensure_single_price(FlowFromGridSourceType val)
Definition: data.py:156
None async_load(HomeAssistant hass)
None async_delay_save(self, Callable[[], _T] data_func, float delay=0)
Definition: storage.py:444