mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Plant (replacement for MiGardener) (#7131)
* new implementation without mqtt * fixed lint findings * fixed more lint findings * fixed final flak8 error * added unit tests for platform "plant" * - changed status to "OK" / "problem" - added attribute "problem" with details on the problems - removed unused constant - setting icon to "?" until we have meaningful data * reformatted code to meet line length requirements
This commit is contained in:
parent
c14b829f27
commit
5d7403bd81
242
homeassistant/components/plant.py
Normal file
242
homeassistant/components/plant.py
Normal file
@ -0,0 +1,242 @@
|
||||
"""Component to monitor plants.
|
||||
|
||||
This is meant to be used with Xiaomi Mi Plant sensors, but will
|
||||
work with any sensor that provides the right parameters.
|
||||
To read the sensor data and send it via MQTT,
|
||||
see https://github.com/ChristianKuehnel/plantgateway
|
||||
"""
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import (
|
||||
STATE_UNKNOWN, TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_SENSORS,
|
||||
ATTR_UNIT_OF_MEASUREMENT, ATTR_ICON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'plant'
|
||||
|
||||
READING_BATTERY = 'battery'
|
||||
READING_TEMPERATURE = ATTR_TEMPERATURE
|
||||
READING_MOISTURE = 'moisture'
|
||||
READING_CONDUCTIVITY = 'conductivity'
|
||||
READING_BRIGHTNESS = 'brightness'
|
||||
|
||||
ATTR_PROBLEM = 'problem'
|
||||
PROBLEM_NONE = 'none'
|
||||
|
||||
CONF_MIN_BATTERY_LEVEL = 'min_' + READING_BATTERY
|
||||
CONF_MIN_TEMPERATURE = 'min_' + READING_TEMPERATURE
|
||||
CONF_MAX_TEMPERATURE = 'max_' + READING_TEMPERATURE
|
||||
CONF_MIN_MOISTURE = 'min_' + READING_MOISTURE
|
||||
CONF_MAX_MOISTURE = 'max_' + READING_MOISTURE
|
||||
CONF_MIN_CONDUCTIVITY = 'min_' + READING_CONDUCTIVITY
|
||||
CONF_MAX_CONDUCTIVITY = 'max_' + READING_CONDUCTIVITY
|
||||
CONF_MIN_BRIGHTNESS = 'min_' + READING_BRIGHTNESS
|
||||
CONF_MAX_BRIGHTNESS = 'max_' + READING_BRIGHTNESS
|
||||
|
||||
CONF_SENSOR_BATTERY_LEVEL = READING_BATTERY
|
||||
CONF_SENSOR_MOISTURE = READING_MOISTURE
|
||||
CONF_SENSOR_CONDUCTIVITY = READING_CONDUCTIVITY
|
||||
CONF_SENSOR_TEMPERATURE = READING_TEMPERATURE
|
||||
CONF_SENSOR_BRIGHTNESS = READING_BRIGHTNESS
|
||||
|
||||
SCHEMA_SENSORS = vol.Schema({
|
||||
vol.Optional(CONF_SENSOR_BATTERY_LEVEL): cv.entity_id,
|
||||
vol.Optional(CONF_SENSOR_MOISTURE): cv.entity_id,
|
||||
vol.Optional(CONF_SENSOR_CONDUCTIVITY): cv.entity_id,
|
||||
vol.Optional(CONF_SENSOR_TEMPERATURE): cv.entity_id,
|
||||
vol.Optional(CONF_SENSOR_BRIGHTNESS): cv.entity_id,
|
||||
})
|
||||
|
||||
PLANT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_SENSORS): vol.Schema(SCHEMA_SENSORS),
|
||||
vol.Optional(CONF_MIN_BATTERY_LEVEL): cv.positive_int,
|
||||
vol.Optional(CONF_MIN_TEMPERATURE): cv.small_float,
|
||||
vol.Optional(CONF_MAX_TEMPERATURE): cv.small_float,
|
||||
vol.Optional(CONF_MIN_MOISTURE): cv.positive_int,
|
||||
vol.Optional(CONF_MAX_MOISTURE): cv.positive_int,
|
||||
vol.Optional(CONF_MIN_CONDUCTIVITY): cv.positive_int,
|
||||
vol.Optional(CONF_MAX_CONDUCTIVITY): cv.positive_int,
|
||||
vol.Optional(CONF_MIN_BRIGHTNESS): cv.positive_int,
|
||||
vol.Optional(CONF_MAX_BRIGHTNESS): cv.positive_int,
|
||||
})
|
||||
|
||||
DOMAIN = 'plant'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
cv.string: PLANT_SCHEMA
|
||||
},
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up Plant component."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
entities = []
|
||||
for plant_name, plant_config in config[DOMAIN].items():
|
||||
_LOGGER.info('added plant %s', plant_name)
|
||||
entity = Plant(plant_name, plant_config)
|
||||
sensor_entity_ids = list(plant_config[CONF_SENSORS].values())
|
||||
_LOGGER.debug('subscribing to entity_ids %s', sensor_entity_ids)
|
||||
async_track_state_change(hass, sensor_entity_ids, entity.state_changed)
|
||||
entities.append(entity)
|
||||
|
||||
yield from component.async_add_entities(entities)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Plant(Entity):
|
||||
"""Plant monitors the well-being of a plant.
|
||||
|
||||
It also checks the measurements against
|
||||
configurable min and max values.
|
||||
"""
|
||||
|
||||
READINGS = {
|
||||
READING_BATTERY: {
|
||||
ATTR_UNIT_OF_MEASUREMENT: '%',
|
||||
'min': CONF_MIN_BATTERY_LEVEL,
|
||||
'icon': 'mdi:battery-outline'
|
||||
},
|
||||
READING_TEMPERATURE: {
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
|
||||
'min': CONF_MIN_TEMPERATURE,
|
||||
'max': CONF_MAX_TEMPERATURE,
|
||||
'icon': 'mdi:thermometer'
|
||||
},
|
||||
READING_MOISTURE: {
|
||||
ATTR_UNIT_OF_MEASUREMENT: '%',
|
||||
'min': CONF_MIN_MOISTURE,
|
||||
'max': CONF_MAX_MOISTURE,
|
||||
'icon': 'mdi:water'
|
||||
},
|
||||
READING_CONDUCTIVITY: {
|
||||
ATTR_UNIT_OF_MEASUREMENT: 'µS/cm',
|
||||
'min': CONF_MIN_CONDUCTIVITY,
|
||||
'max': CONF_MAX_CONDUCTIVITY,
|
||||
'icon': 'mdi:emoticon-poop'
|
||||
},
|
||||
READING_BRIGHTNESS: {
|
||||
ATTR_UNIT_OF_MEASUREMENT: 'lux',
|
||||
'min': CONF_MIN_BRIGHTNESS,
|
||||
'max': CONF_MAX_BRIGHTNESS,
|
||||
'icon': 'mdi:white-balance-sunny'
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, name, config):
|
||||
"""Default constructor."""
|
||||
self._config = config
|
||||
self._sensormap = dict()
|
||||
for reading, entity_id in config['sensors'].items():
|
||||
self._sensormap[entity_id] = reading
|
||||
self._state = STATE_UNKNOWN
|
||||
self._name = name
|
||||
self._battery = None
|
||||
self._moisture = None
|
||||
self._conductivity = None
|
||||
self._temperature = None
|
||||
self._brightness = None
|
||||
self._icon = 'mdi:help-circle'
|
||||
self._problems = PROBLEM_NONE
|
||||
|
||||
@callback
|
||||
def state_changed(self, entity_id, _, new_state):
|
||||
"""Update the sensor status.
|
||||
|
||||
This callback is triggered, when the sensor state changes.
|
||||
"""
|
||||
value = new_state.state
|
||||
_LOGGER.debug('received callback from %s with value %s',
|
||||
entity_id, value)
|
||||
if value == STATE_UNKNOWN:
|
||||
return
|
||||
|
||||
reading = self._sensormap[entity_id]
|
||||
if reading == READING_MOISTURE:
|
||||
self._moisture = int(value)
|
||||
elif reading == READING_BATTERY:
|
||||
self._battery = int(value)
|
||||
elif reading == READING_TEMPERATURE:
|
||||
self._temperature = float(value)
|
||||
elif reading == READING_CONDUCTIVITY:
|
||||
self._conductivity = int(value)
|
||||
elif reading == READING_BRIGHTNESS:
|
||||
self._brightness = int(value)
|
||||
else:
|
||||
raise _LOGGER.error('unknown reading from sensor %s: %s',
|
||||
entity_id, value)
|
||||
self._update_state()
|
||||
|
||||
def _update_state(self):
|
||||
""""Update the state of the class based sensor data."""
|
||||
result = []
|
||||
for sensor_name in self._sensormap.values():
|
||||
params = self.READINGS[sensor_name]
|
||||
value = getattr(self, '_{}'.format(sensor_name))
|
||||
if value is not None:
|
||||
if 'min' in params and params['min'] in self._config:
|
||||
min_value = self._config[params['min']]
|
||||
if value < min_value:
|
||||
result.append('{} low'.format(sensor_name))
|
||||
self._icon = params['icon']
|
||||
|
||||
if 'max' in params and params['max'] in self._config:
|
||||
max_value = self._config[params['max']]
|
||||
if value > max_value:
|
||||
result.append('{} high'.format(sensor_name))
|
||||
self._icon = params['icon']
|
||||
|
||||
if len(result) == 0:
|
||||
self._state = 'ok'
|
||||
self._icon = 'mdi:thumb-up'
|
||||
self._problems = PROBLEM_NONE
|
||||
else:
|
||||
self._state = 'problem'
|
||||
self._problems = ','.join(result)
|
||||
_LOGGER.debug('new data processed')
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the attributes of the entity.
|
||||
|
||||
Provide the individual measurements from the
|
||||
sensor in the attributes of the device.
|
||||
"""
|
||||
attrib = {
|
||||
ATTR_ICON: self._icon,
|
||||
ATTR_PROBLEM: self._problems,
|
||||
}
|
||||
|
||||
for reading in self._sensormap.values():
|
||||
attrib[reading] = getattr(self, '_{}'.format(reading))
|
||||
|
||||
return attrib
|
73
tests/components/test_plant.py
Normal file
73
tests/components/test_plant.py
Normal file
@ -0,0 +1,73 @@
|
||||
"""Unit tests for platform/plant.py."""
|
||||
|
||||
import unittest
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
import homeassistant.components.plant as plant
|
||||
|
||||
|
||||
class TestPlant(unittest.TestCase):
|
||||
"""test the processing of data."""
|
||||
|
||||
GOOD_DATA = {
|
||||
'moisture': 50,
|
||||
'battery': 90,
|
||||
'temperature': 23.4,
|
||||
'conductivity': 777,
|
||||
'brightness': 987,
|
||||
}
|
||||
|
||||
GOOD_CONFIG = {
|
||||
'sensors': {
|
||||
'moisture': 'sensor.mqtt_plant_moisture',
|
||||
'battery': 'sensor.mqtt_plant_battery',
|
||||
'temperature': 'sensor.mqtt_plant_temperature',
|
||||
'conductivity': 'sensor.mqtt_plant_conductivity',
|
||||
'brightness': 'sensor.mqtt_plant_brightness',
|
||||
},
|
||||
'min_moisture': 20,
|
||||
'max_moisture': 60,
|
||||
'min_battery': 17,
|
||||
'min_conductivity': 500,
|
||||
'min_temperature': 15,
|
||||
}
|
||||
|
||||
class _MockState(object):
|
||||
|
||||
def __init__(self, state=None):
|
||||
self.state = state
|
||||
|
||||
def setUp(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_valid_data(self):
|
||||
"""Test processing valid data."""
|
||||
self.sensor = plant.Plant('my plant', self.GOOD_CONFIG)
|
||||
self.sensor.hass = self.hass
|
||||
for reading, value in self.GOOD_DATA.items():
|
||||
self.sensor.state_changed(
|
||||
self.GOOD_CONFIG['sensors'][reading], None,
|
||||
TestPlant._MockState(value))
|
||||
self.assertEqual(self.sensor.state, 'ok')
|
||||
attrib = self.sensor.state_attributes
|
||||
for reading, value in self.GOOD_DATA.items():
|
||||
# battery level has a different name in
|
||||
# the JSON format than in hass
|
||||
self.assertEqual(attrib[reading], value)
|
||||
|
||||
def test_low_battery(self):
|
||||
"""Test processing with low battery data and limit set."""
|
||||
self.sensor = plant.Plant(self.hass, self.GOOD_CONFIG)
|
||||
self.sensor.hass = self.hass
|
||||
self.assertEqual(self.sensor.state_attributes['problem'], 'none')
|
||||
self.sensor.state_changed('sensor.mqtt_plant_battery',
|
||||
TestPlant._MockState(45),
|
||||
TestPlant._MockState(10))
|
||||
self.assertEqual(self.sensor.state, 'problem')
|
||||
self.assertEqual(self.sensor.state_attributes['problem'],
|
||||
'battery low')
|
Loading…
x
Reference in New Issue
Block a user