Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Component to count within automations."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any, Self
7 
8 import voluptuous as vol
9 
10 from homeassistant.const import (
11  ATTR_EDITABLE,
12  CONF_ICON,
13  CONF_ID,
14  CONF_MAXIMUM,
15  CONF_MINIMUM,
16  CONF_NAME,
17 )
18 from homeassistant.core import HomeAssistant, callback
19 from homeassistant.helpers import collection
21 from homeassistant.helpers.entity_component import EntityComponent
22 from homeassistant.helpers.restore_state import RestoreEntity
23 from homeassistant.helpers.storage import Store
24 from homeassistant.helpers.typing import ConfigType, VolDictType
25 
26 _LOGGER = logging.getLogger(__name__)
27 
28 ATTR_INITIAL = "initial"
29 ATTR_STEP = "step"
30 ATTR_MINIMUM = "minimum"
31 ATTR_MAXIMUM = "maximum"
32 VALUE = "value"
33 
34 CONF_INITIAL = "initial"
35 CONF_RESTORE = "restore"
36 CONF_STEP = "step"
37 
38 DEFAULT_INITIAL = 0
39 DEFAULT_STEP = 1
40 DOMAIN = "counter"
41 
42 ENTITY_ID_FORMAT = DOMAIN + ".{}"
43 
44 SERVICE_DECREMENT = "decrement"
45 SERVICE_INCREMENT = "increment"
46 SERVICE_RESET = "reset"
47 SERVICE_SET_VALUE = "set_value"
48 
49 STORAGE_KEY = DOMAIN
50 STORAGE_VERSION = 1
51 
52 STORAGE_FIELDS: VolDictType = {
53  vol.Optional(CONF_ICON): cv.icon,
54  vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int,
55  vol.Required(CONF_NAME): vol.All(cv.string, vol.Length(min=1)),
56  vol.Optional(CONF_MAXIMUM, default=None): vol.Any(None, vol.Coerce(int)),
57  vol.Optional(CONF_MINIMUM, default=None): vol.Any(None, vol.Coerce(int)),
58  vol.Optional(CONF_RESTORE, default=True): cv.boolean,
59  vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int,
60 }
61 
62 
63 def _none_to_empty_dict[_T](value: _T | None) -> _T | dict[str, Any]:
64  if value is None:
65  return {}
66  return value
67 
68 
69 CONFIG_SCHEMA = vol.Schema(
70  {
71  DOMAIN: cv.schema_with_slug_keys(
72  vol.All(
73  _none_to_empty_dict,
74  {
75  vol.Optional(CONF_ICON): cv.icon,
76  vol.Optional(
77  CONF_INITIAL, default=DEFAULT_INITIAL
78  ): cv.positive_int,
79  vol.Optional(CONF_NAME): cv.string,
80  vol.Optional(CONF_MAXIMUM, default=None): vol.Any(
81  None, vol.Coerce(int)
82  ),
83  vol.Optional(CONF_MINIMUM, default=None): vol.Any(
84  None, vol.Coerce(int)
85  ),
86  vol.Optional(CONF_RESTORE, default=True): cv.boolean,
87  vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int,
88  },
89  )
90  )
91  },
92  extra=vol.ALLOW_EXTRA,
93 )
94 
95 
96 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
97  """Set up the counters."""
98  component = EntityComponent[Counter](_LOGGER, DOMAIN, hass)
99  id_manager = collection.IDManager()
100 
101  yaml_collection = collection.YamlCollection(
102  logging.getLogger(f"{__name__}.yaml_collection"), id_manager
103  )
104  collection.sync_entity_lifecycle(
105  hass, DOMAIN, DOMAIN, component, yaml_collection, Counter
106  )
107 
108  storage_collection = CounterStorageCollection(
109  Store(hass, STORAGE_VERSION, STORAGE_KEY),
110  id_manager,
111  )
112  collection.sync_entity_lifecycle(
113  hass, DOMAIN, DOMAIN, component, storage_collection, Counter
114  )
115 
116  await yaml_collection.async_load(
117  [{CONF_ID: id_, **(conf or {})} for id_, conf in config.get(DOMAIN, {}).items()]
118  )
119  await storage_collection.async_load()
120 
121  collection.DictStorageCollectionWebsocket(
122  storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
123  ).async_setup(hass)
124 
125  component.async_register_entity_service(SERVICE_INCREMENT, None, "async_increment")
126  component.async_register_entity_service(SERVICE_DECREMENT, None, "async_decrement")
127  component.async_register_entity_service(SERVICE_RESET, None, "async_reset")
128  component.async_register_entity_service(
129  SERVICE_SET_VALUE,
130  {vol.Required(VALUE): cv.positive_int},
131  "async_set_value",
132  )
133 
134  return True
135 
136 
137 class CounterStorageCollection(collection.DictStorageCollection):
138  """Input storage based collection."""
139 
140  CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS)
141 
142  async def _process_create_data(self, data: dict) -> dict:
143  """Validate the config is valid."""
144  return self.CREATE_UPDATE_SCHEMACREATE_UPDATE_SCHEMA(data) # type: ignore[no-any-return]
145 
146  @callback
147  def _get_suggested_id(self, info: dict) -> str:
148  """Suggest an ID based on the config."""
149  return info[CONF_NAME] # type: ignore[no-any-return]
150 
151  async def _update_data(self, item: dict, update_data: dict) -> dict:
152  """Return a new updated data object."""
153  update_data = self.CREATE_UPDATE_SCHEMACREATE_UPDATE_SCHEMA(update_data)
154  return {CONF_ID: item[CONF_ID]} | update_data
155 
156 
157 class Counter(collection.CollectionEntity, RestoreEntity):
158  """Representation of a counter."""
159 
160  _attr_should_poll: bool = False
161  editable: bool
162 
163  def __init__(self, config: ConfigType) -> None:
164  """Initialize a counter."""
165  self._config_config: ConfigType = config
166  self._state_state: int | None = config[CONF_INITIAL]
167 
168  @classmethod
169  def from_storage(cls, config: ConfigType) -> Self:
170  """Create counter instance from storage."""
171  counter = cls(config)
172  counter.editable = True
173  return counter
174 
175  @classmethod
176  def from_yaml(cls, config: ConfigType) -> Self:
177  """Create counter instance from yaml config."""
178  counter = cls(config)
179  counter.editable = False
180  counter.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID])
181  return counter
182 
183  @property
184  def name(self) -> str | None:
185  """Return name of the counter."""
186  return self._config_config.get(CONF_NAME)
187 
188  @property
189  def icon(self) -> str | None:
190  """Return the icon to be used for this entity."""
191  return self._config_config.get(CONF_ICON)
192 
193  @property
194  def state(self) -> int | None:
195  """Return the current value of the counter."""
196  return self._state_state
197 
198  @property
199  def extra_state_attributes(self) -> dict:
200  """Return the state attributes."""
201  ret = {
202  ATTR_EDITABLE: self.editable,
203  ATTR_INITIAL: self._config_config[CONF_INITIAL],
204  ATTR_STEP: self._config_config[CONF_STEP],
205  }
206  if self._config_config[CONF_MINIMUM] is not None:
207  ret[CONF_MINIMUM] = self._config_config[CONF_MINIMUM]
208  if self._config_config[CONF_MAXIMUM] is not None:
209  ret[CONF_MAXIMUM] = self._config_config[CONF_MAXIMUM]
210  return ret
211 
212  @property
213  def unique_id(self) -> str | None:
214  """Return unique id of the entity."""
215  return self._config_config[CONF_ID] # type: ignore[no-any-return]
216 
217  def compute_next_state(self, state: int | None) -> int | None:
218  """Keep the state within the range of min/max values."""
219  if self._config_config[CONF_MINIMUM] is not None:
220  state = max(self._config_config[CONF_MINIMUM], state)
221  if self._config_config[CONF_MAXIMUM] is not None:
222  state = min(self._config_config[CONF_MAXIMUM], state)
223 
224  return state
225 
226  async def async_added_to_hass(self) -> None:
227  """Call when entity about to be added to Home Assistant."""
228  await super().async_added_to_hass()
229  # __init__ will set self._state to self._initial, only override
230  # if needed.
231  if (
232  self._config_config[CONF_RESTORE]
233  and (state := await self.async_get_last_stateasync_get_last_state()) is not None
234  ):
235  self._state_state = self.compute_next_statecompute_next_state(int(state.state))
236 
237  @callback
238  def async_decrement(self) -> None:
239  """Decrement the counter."""
240  self._state_state = self.compute_next_statecompute_next_state(self._state_state - self._config_config[CONF_STEP])
241  self.async_write_ha_stateasync_write_ha_state()
242 
243  @callback
244  def async_increment(self) -> None:
245  """Increment a counter."""
246  self._state_state = self.compute_next_statecompute_next_state(self._state_state + self._config_config[CONF_STEP])
247  self.async_write_ha_stateasync_write_ha_state()
248 
249  @callback
250  def async_reset(self) -> None:
251  """Reset a counter."""
252  self._state_state = self.compute_next_statecompute_next_state(self._config_config[CONF_INITIAL])
253  self.async_write_ha_stateasync_write_ha_state()
254 
255  @callback
256  def async_set_value(self, value: int) -> None:
257  """Set counter to value."""
258  if (maximum := self._config_config.get(CONF_MAXIMUM)) is not None and value > maximum:
259  raise ValueError(
260  f"Value {value} for {self.entity_id} exceeding the maximum value of {maximum}"
261  )
262 
263  if (minimum := self._config_config.get(CONF_MINIMUM)) is not None and value < minimum:
264  raise ValueError(
265  f"Value {value} for {self.entity_id} exceeding the minimum value of {minimum}"
266  )
267 
268  if (step := self._config_config.get(CONF_STEP)) is not None and value % step != 0:
269  raise ValueError(
270  f"Value {value} for {self.entity_id} is not a multiple of the step size {step}"
271  )
272 
273  self._state_state = value
274  self.async_write_ha_stateasync_write_ha_state()
275 
276  async def async_update_config(self, config: ConfigType) -> None:
277  """Change the counter's settings WS CRUD."""
278  self._config_config = config
279  self._state_state = self.compute_next_statecompute_next_state(self._state_state)
280  self.async_write_ha_stateasync_write_ha_state()
dict _update_data(self, dict item, dict update_data)
Definition: __init__.py:151
None __init__(self, ConfigType config)
Definition: __init__.py:163
Self from_yaml(cls, ConfigType config)
Definition: __init__.py:176
int|None compute_next_state(self, int|None state)
Definition: __init__.py:217
None async_update_config(self, ConfigType config)
Definition: __init__.py:276
Self from_storage(cls, ConfigType config)
Definition: __init__.py:169
None async_set_value(self, int value)
Definition: __init__.py:256
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:96