1 """Support for monitoring plants.
3 DEVELOPMENT OF THE PLANT INTEGRATION IS FROZEN
4 PENDING A DESIGN EVALUATION.
7 from collections
import deque
8 from contextlib
import suppress
9 from datetime
import datetime, timedelta
12 import voluptuous
as vol
16 ATTR_UNIT_OF_MEASUREMENT,
29 EventStateChangedData,
43 ATTR_DICT_OF_UNITS_OF_MEASUREMENT,
44 ATTR_MAX_BRIGHTNESS_HISTORY,
49 CONF_MAX_CONDUCTIVITY,
52 CONF_MIN_BATTERY_LEVEL,
54 CONF_MIN_CONDUCTIVITY,
58 DEFAULT_MAX_CONDUCTIVITY,
60 DEFAULT_MIN_BATTERY_LEVEL,
61 DEFAULT_MIN_CONDUCTIVITY,
72 _LOGGER = logging.getLogger(__name__)
74 CONF_SENSOR_BATTERY_LEVEL = READING_BATTERY
75 CONF_SENSOR_MOISTURE = READING_MOISTURE
76 CONF_SENSOR_CONDUCTIVITY = READING_CONDUCTIVITY
77 CONF_SENSOR_TEMPERATURE = READING_TEMPERATURE
78 CONF_SENSOR_BRIGHTNESS = READING_BRIGHTNESS
81 SCHEMA_SENSORS = vol.Schema(
83 vol.Optional(CONF_SENSOR_BATTERY_LEVEL): cv.entity_id,
84 vol.Optional(CONF_SENSOR_MOISTURE): cv.entity_id,
85 vol.Optional(CONF_SENSOR_CONDUCTIVITY): cv.entity_id,
86 vol.Optional(CONF_SENSOR_TEMPERATURE): cv.entity_id,
87 vol.Optional(CONF_SENSOR_BRIGHTNESS): cv.entity_id,
91 PLANT_SCHEMA = vol.Schema(
93 vol.Required(CONF_SENSORS): vol.Schema(SCHEMA_SENSORS),
95 CONF_MIN_BATTERY_LEVEL, default=DEFAULT_MIN_BATTERY_LEVEL
97 vol.Optional(CONF_MIN_TEMPERATURE): vol.Coerce(float),
98 vol.Optional(CONF_MAX_TEMPERATURE): vol.Coerce(float),
99 vol.Optional(CONF_MIN_MOISTURE, default=DEFAULT_MIN_MOISTURE): cv.positive_int,
100 vol.Optional(CONF_MAX_MOISTURE, default=DEFAULT_MAX_MOISTURE): cv.positive_int,
102 CONF_MIN_CONDUCTIVITY, default=DEFAULT_MIN_CONDUCTIVITY
105 CONF_MAX_CONDUCTIVITY, default=DEFAULT_MAX_CONDUCTIVITY
107 vol.Optional(CONF_MIN_BRIGHTNESS): cv.positive_int,
108 vol.Optional(CONF_MAX_BRIGHTNESS): cv.positive_int,
109 vol.Optional(CONF_CHECK_DAYS, default=DEFAULT_CHECK_DAYS): cv.positive_int,
113 CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: PLANT_SCHEMA}}, extra=vol.ALLOW_EXTRA)
116 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
117 """Set up the Plant component."""
118 component = EntityComponent[Plant](_LOGGER, DOMAIN, hass)
121 for plant_name, plant_config
in config[DOMAIN].items():
122 _LOGGER.info(
"Added plant %s", plant_name)
123 entity =
Plant(plant_name, plant_config)
124 entities.append(entity)
126 await component.async_add_entities(entities)
131 """Plant monitors the well-being of a plant.
133 It also checks the measurements against
134 configurable min and max values.
136 DEVELOPMENT OF THE PLANT INTEGRATION IS FROZEN
137 PENDING A DESIGN EVALUATION.
140 _attr_should_poll =
False
144 ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE,
145 "min": CONF_MIN_BATTERY_LEVEL,
147 READING_TEMPERATURE: {
148 ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
149 "min": CONF_MIN_TEMPERATURE,
150 "max": CONF_MAX_TEMPERATURE,
153 ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE,
154 "min": CONF_MIN_MOISTURE,
155 "max": CONF_MAX_MOISTURE,
157 READING_CONDUCTIVITY: {
158 ATTR_UNIT_OF_MEASUREMENT: UnitOfConductivity.MICROSIEMENS_PER_CM,
159 "min": CONF_MIN_CONDUCTIVITY,
160 "max": CONF_MAX_CONDUCTIVITY,
162 READING_BRIGHTNESS: {
163 ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX,
164 "min": CONF_MIN_BRIGHTNESS,
165 "max": CONF_MAX_BRIGHTNESS,
170 """Initialize the Plant component."""
175 for reading, entity_id
in config[
"sensors"].items():
176 self.
_sensormap_sensormap[entity_id] = reading
188 if CONF_CHECK_DAYS
in self.
_config_config:
194 """Sensor state change event."""
195 self.
state_changedstate_changed(event.data[
"entity_id"], event.data[
"new_state"])
199 """Update the sensor status."""
200 if new_state
is None:
203 value = new_state.state
204 _LOGGER.debug(
"Received callback from %s with value %s", entity_id, value)
205 if value == STATE_UNKNOWN:
208 reading = self.
_sensormap_sensormap[entity_id]
209 if reading == READING_MOISTURE:
210 if value != STATE_UNAVAILABLE:
213 elif reading == READING_BATTERY:
214 if value != STATE_UNAVAILABLE:
217 elif reading == READING_TEMPERATURE:
218 if value != STATE_UNAVAILABLE:
221 elif reading == READING_CONDUCTIVITY:
222 if value != STATE_UNAVAILABLE:
225 elif reading == READING_BRIGHTNESS:
226 if value != STATE_UNAVAILABLE:
230 self.
_brightness_brightness, new_state.last_updated
234 f
"Unknown reading from sensor {entity_id}: {value}"
236 if ATTR_UNIT_OF_MEASUREMENT
in new_state.attributes:
238 ATTR_UNIT_OF_MEASUREMENT
243 """Update the state of the class based sensor data."""
245 for sensor_name
in self.
_sensormap_sensormap.values():
246 params = self.
READINGSREADINGS[sensor_name]
247 if (value := getattr(self, f
"_{sensor_name}"))
is not None:
248 if value == STATE_UNAVAILABLE:
249 result.append(f
"{sensor_name} unavailable")
251 if sensor_name == READING_BRIGHTNESS:
258 result.append(self.
_check_min_check_min(sensor_name, value, params))
259 result.append(self.
_check_max_check_max(sensor_name, value, params))
261 result = [r
for r
in result
if r
is not None]
264 self.
_state_state = STATE_PROBLEM
265 self.
_problems_problems =
", ".join(result)
267 self.
_state_state = STATE_OK
269 _LOGGER.debug(
"New data processed")
273 """If configured, check the value against the defined minimum value."""
274 if "min" in params
and params[
"min"]
in self.
_config_config:
275 min_value = self.
_config_config[params[
"min"]]
276 if value < min_value:
277 return f
"{sensor_name} low"
281 """If configured, check the value against the defined maximum value."""
282 if "max" in params
and params[
"max"]
in self.
_config_config:
283 max_value = self.
_config_config[params[
"max"]]
284 if value > max_value:
285 return f
"{sensor_name} high"
289 """After being added to hass, load from history."""
290 if "recorder" in self.
hasshass.config.components:
302 if (state := self.
hasshass.states.get(entity_id))
is not None:
306 """Load the history of the brightness values from the database.
308 This only needs to be done once during startup.
313 if entity_id
is None:
315 "Not reading the history from the database as "
316 "there is no brightness sensor configured"
319 _LOGGER.debug(
"Initializing values for %s from the database", self.
_name_name)
320 lower_entity_id = entity_id.lower()
321 history_list = history.state_changes_during_period(
324 entity_id=lower_entity_id,
327 for state
in history_list.get(lower_entity_id, []):
330 with suppress(ValueError):
332 int(state.state), state.last_updated
335 _LOGGER.debug(
"Initializing from database completed")
339 """Return the name of the sensor."""
340 return self.
_name_name
344 """Return the state of the entity."""
349 """Return the attributes of the entity.
351 Provide the individual measurements from the
352 sensor in the attributes of the device.
360 for reading
in self.
_sensormap_sensormap.values():
361 attrib[reading] = getattr(self, f
"_{reading}")
370 """Stores one measurement per day for a maximum number of days.
372 At the moment only the maximum value per day is kept.
374 DEVELOPMENT OF THE PLANT INTEGRATION IS FROZEN
375 PENDING A DESIGN EVALUATION.
379 """Create new DailyHistory with a maximum length of the history."""
386 """Add a new measurement for a certain day."""
387 day = (timestamp
or datetime.now()).
date()
388 if not isinstance(value, (int, float)):
390 if self.
_days_days
is None:
391 self.
_days_days = deque()
394 current_day = self.
_days_days[-1]
395 if day == current_day:
397 elif day > current_day:
400 _LOGGER.warning(
"Received old measurement, not storing it")
405 """Add a new day to the history.
407 Deletes the oldest day, if the queue becomes too long.
410 oldest = self.
_days_days.popleft()
412 self.
_days_days.append(day)
413 if not isinstance(value, (int, float)):
def _add_day(self, day, value)
def add_measurement(self, value, timestamp=None)
def __init__(self, max_length)
def _load_history_from_db(self)
def _check_min(self, sensor_name, value, params)
None state_changed(self, str entity_id, State|None new_state)
def extra_state_attributes(self)
def _check_max(self, sensor_name, value, params)
def async_added_to_hass(self)
None _state_changed_event(self, Event[EventStateChangedData] event)
def __init__(self, name, config)
None async_write_ha_state(self)
web.Response get(self, web.Request request, str config_key)
bool async_setup(HomeAssistant hass, ConfigType config)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
Recorder get_instance(HomeAssistant hass)