Skip to content

Commit

Permalink
Discovery can be deleted when component removed, code beautifications
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinkk525 committed Nov 1, 2019
1 parent 08a02e9 commit 61e6987
Show file tree
Hide file tree
Showing 37 changed files with 325 additions and 255 deletions.
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,24 @@ Every loaded component will be published as *log.info* to *<home>/log/info/<devi

### API

Components can be and do anything. There is a basic API and [base class](./pysmartnode/utils/component/__init__.py) which takes care of mqtt, discovery and the basic API.
<br>Sensors all have a similar API to have a standardized usage.
For example all temperature sensors provide the function (coroutine actually) *sensor.temperature(publish=True)* that returns the temperature as float and takes the argument *publish=True* controlling if the read value should be published to mqtt in addition to reading the value.
This should make working with different types of sensors easier. If you are e.g. building a heating controller and need a temperature from some sensor, you can just connect any sensor and provide the heating code with that sensor by configuration.
As every temperature sensor has the function (actually coroutine) *temperature()* returning the current temperature in float (or None on error) it does not care about which sensor is connected.
<br>Switch components can be integrated even easier using the [switch base class](./pysmartnode/utils/component/switch.py).
Components can be and do anything. There is a basic API and [base class](./pysmartnode/utils/component/__init__.py) which helps with homeassistant mqtt discovery and the basic API.
<br>Sensors all have a similar API to have a standardized usage through the [sensor base class](./pysmartnode/utils/component/sensor.py).
The sensor base class makes developing sensors very easy as it takes care of mqtt discovery, reading and publishing intervals and the standardized API.
All sensors now have a common API:
- getValue(sensor_type)->sensor_type last read value
- getTopic(sensor_type)->mqtt topic of sensor_type
- getTemplate(sensor_type)->homeassistant value template of sensor_type
- getTimestamp(sensor_type)->timestamp of last successful sensor reading
- getReadingsEvent()->Event being set on next sensor reading

<br>Sensor_types in definitions but can be custom, those are only the ones supported by Homeassistant.

<br>Common features:
- Reading interval and publish interval separated and not impacting each other
- Reading and publish intervals can be changed during runtime, optionally by mqtt
- Sensor subclass only needs to implement a _read() function reading the sensor and submitting the read values. Base class does everything else (publishing, discovery, ...)
<br>This should make working with different types of sensors easier. If you are e.g. building a heating controller and need a temperature from some sensor, you can just connect any sensor and provide the heating code with that sensor by configuration.
<br>Switch components can be integrated similarily easy using the [switch base class](./pysmartnode/utils/component/switch.py).
<br>Templates for how to use the components can be found in [templates](./_templates).

### MQTT-Discovery
Expand Down
30 changes: 19 additions & 11 deletions _templates/component_template.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
'''
Created on 2018-06-22
@author: Kevin Köck
'''
# Author: Kevin Köck
# Copyright Kevin Köck 2018 Released under the MIT license
# Created on 2018-06-22

"""
example config for MyComponent:
Expand All @@ -13,13 +11,14 @@
my_value: "hi there"
# mqtt_topic: sometopic # optional, defaults to home/<controller-id>/<component_name>/<component-count>/set
# mqtt_topic2: sometopic # optional, defautls to home/sometopic
# friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery
# friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery
# discover: true # optional, if false no discovery message for homeassistant will be sent.
}
}
"""

__updated__ = "2019-10-31"
__version__ = "1.6"
__updated__ = "2019-11-01"
__version__ = "1.7"

import uasyncio as asyncio
from pysmartnode import config
Expand Down Expand Up @@ -114,16 +113,25 @@ async def _remove(self):
pass
await super()._remove()

async def _discovery(self):
async def _discovery(self, register=True):
"""
Send discovery messages
:param register: if True send discovery message, if False send empty discovery message
to remove the component from homeassistant.
:return:
"""
name = "{!s}{!s}".format(COMPONENT_NAME, self._count)
component_topic = _mqtt.getDeviceTopic(name)
# component topic could be something completely user defined.
# No need to follow the pattern:
component_topic = self._command_topic[:-4] # get the state topic of custom component topic
friendly_name = self._frn # define a friendly name for the homeassistant gui.
# Doesn't need to be unique
await self._publishDiscovery(_COMPONENT_TYPE, component_topic, name, DISCOVERY_SWITCH,
friendly_name)
if register:
await self._publishDiscovery(_COMPONENT_TYPE, component_topic, name, DISCOVERY_SWITCH,
friendly_name)
else:
await self._deleteDiscovery(_COMPONENT_TYPE, name)
del name, component_topic, friendly_name
gc.collect()

Expand Down
2 changes: 1 addition & 1 deletion _templates/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
}

# Alternatively or additionally you can register components manually,
# which saves a lot of RAM as the dict doesn't get loaded into RAM.
# which saves a lot of RAM as no dict doesn't get loaded into RAM.
# This example provides the same configuration as the COMPONENT dict above:

from pysmartnode import config
Expand Down
9 changes: 6 additions & 3 deletions dev/ecMeter.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,15 @@ async def _init(self):
await gen()
await asyncio.sleep(interval)

async def _discovery(self):
async def _discovery(self, register=True):
sens = '"unit_of_meas":"mS",' \
'"val_tpl":"{{ value|float }}",'
name = "{!s}{!s}{!s}".format(COMPONENT_NAME, self._count, "EC25")
await self._publishDiscovery(_COMPONENT_TYPE, self._topic_ec, name, sens,
self._frn_ec or "EC25")
if register:
await self._publishDiscovery(_COMPONENT_TYPE, self._topic_ec, name, sens,
self._frn_ec or "EC25")
else:
await self._deleteDiscovery(_COMPONENT_TYPE, name)
del sens, name
gc.collect()
sens = '"unit_of_meas":"ppm",' \
Expand Down
30 changes: 19 additions & 11 deletions dev/moisture.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ async def _read(self, publish=True, timeout=5) -> list:
return [None] * len(sensors)
return res

async def _discovery(self):
async def _discovery(self, register=True):
amux = not isinstance(self._adc, pyADC)
if type(self._sensor_types) == list:
im = len(self._sensor_types)
Expand All @@ -187,18 +187,26 @@ async def _discovery(self):
for i in range(im):
if self._pub_cv:
name = "{!s}{!s}CV".format(COMPONENT_NAME, i)
sens = DISCOVERY_BINARY_SENSOR.format("moisture") # device_class
t = "{!s}/{!s}/conv".format(self.humidityTopic(), i)
await self._publishDiscovery("binary_sensor", t, name, sens,
self._frn_cv or "Moisture")
if register:
sens = DISCOVERY_BINARY_SENSOR.format("moisture") # device_class
t = "{!s}/{!s}/conv".format(self.humidityTopic(), i)
await self._publishDiscovery("binary_sensor", t, name, sens,
self._frn_cv or "Moisture")
del sens
else:
await self._deleteDiscovery("binary_sensor", name)
name = "{!s}{!s}".format(COMPONENT_NAME, i)
t = "{!s}/{!s}".format(self.humidityTopic(), i)
sens = self._composeSensorType("humidity", # device_class
"%", # unit_of_measurement
_VAL_T_HUMIDITY) # value_template
await self._publishDiscovery(_COMPONENT_TYPE, t, name, sens,
self._frn or "Moisture rel.")
del name, sens, t
if register:
sens = self._composeSensorType("humidity", # device_class
"%", # unit_of_measurement
_VAL_T_HUMIDITY) # value_template
await self._publishDiscovery(_COMPONENT_TYPE, t, name, sens,
self._frn or "Moisture rel.")
del sens
else:
await self._deleteDiscovery("binary_sensor", name)
del name, t
gc.collect()

async def humidity(self, publish=True, timeout=5, no_stale=False) -> list:
Expand Down
13 changes: 8 additions & 5 deletions dev/phSensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
growing solution 3.24 3.1 (this is very wrong.., ph actually ~5.2)
"""

__updated__ = "2019-10-27"
__version__ = "0.5"
__updated__ = "2019-11-01"
__version__ = "0.6"

from pysmartnode import config
from pysmartnode.components.machine.adc import ADC
Expand Down Expand Up @@ -92,10 +92,13 @@ async def _loop(self):
self.__ph = await self._read()
await asyncio.sleep(interval)

async def _discovery(self):
async def _discovery(self, register=True):
name = "{!s}{!s}".format(COMPONENT_NAME, self._count)
await self._publishDiscovery(_COMPONENT_TYPE, self.acidityTopic(), name, PH_TYPE,
self._frn or "pH")
if register:
await self._publishDiscovery(_COMPONENT_TYPE, self.acidityTopic(), name, PH_TYPE,
self._frn or "pH")
else:
await self._deleteDiscovery(_COMPONENT_TYPE, name)

async def _read(self, publish=True, timeout=5) -> float:
buf = []
Expand Down
3 changes: 2 additions & 1 deletion pysmartnode/components/devices/arduinoGPIO/arduinoControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
}
"""

from pysmartnode.libraries.arduinoGPIO.arduinoGPIO.arduinoControl import ArduinoControl as _ArduinoControl
from pysmartnode.libraries.arduinoGPIO.arduinoGPIO.arduinoControl import \
ArduinoControl as _ArduinoControl
from pysmartnode.components.machine.pin import Pin as PyPin
from pysmartnode import logging

Expand Down
89 changes: 47 additions & 42 deletions pysmartnode/components/devices/climate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
fan_unit
"""

__updated__ = "2019-10-30"
__version__ = "0.7"
__updated__ = "2019-11-01"
__version__ = "0.8"

from pysmartnode import config
from pysmartnode import logging
Expand Down Expand Up @@ -68,35 +68,11 @@
_count = 0


class BaseMode:
"""
Base class for all modes
"""

def __init__(self, climate):
pass

async def trigger(self, climate, current_temp):
"""Triggered whenever the situation is evaluated again"""
raise NotImplementedError

async def activate(self, climate):
"""Triggered whenever the mode changes and this mode has been activated"""
raise NotImplementedError

async def deactivate(self, climate):
"""Triggered whenever the mode changes and this mode has been deactivated"""
raise NotImplementedError

def __str__(self):
"""Name of the mode, has to be the same as the classname/module"""
raise NotImplementedError


class Climate(Component):
def __init__(self, temperature_sensor, heating_unit, modes: list, interval=300,
temp_step=0.1, min_temp=16, max_temp=28, temp_low=20, temp_high=21,
away_temp_low=16, away_temp_high=17,
def __init__(self, temperature_sensor: ComponentSensor, heating_unit: ComponentSwitch,
modes: list, interval: float = 300, temp_step=0.1, min_temp: float = 16,
max_temp: float = 28, temp_low: float = 20, temp_high: float = 21,
away_temp_low: float = 16, away_temp_high: float = 17,
friendly_name=None, discover=True):
self.checkSensorType(temperature_sensor, SENSOR_TEMPERATURE)
self.checkSwitchType(heating_unit)
Expand All @@ -110,8 +86,8 @@ def __init__(self, temperature_sensor, heating_unit, modes: list, interval=300,
self._temp_step = temp_step
self._min_temp = min_temp
self._max_temp = max_temp
self.temp_sensor = temperature_sensor
self.heating_unit = heating_unit
self.temp_sensor: ComponentSensor = temperature_sensor
self.heating_unit: ComponentSwitch = heating_unit
self._modes = {}
if "off" not in modes:
modes.append("off")
Expand Down Expand Up @@ -302,20 +278,49 @@ async def changeTempLow(self, topic, msg, retain):
self.event.set()
return False

async def _discovery(self):
async def _discovery(self, register=True):
name = "{!s}{!s}".format(COMPONENT_NAME, self._count)
base_topic = _mqtt.getRealTopic(_mqtt.getDeviceTopic(name))
modes = ujson.dumps([str(mode) for mode in self._modes])
gc.collect()
sens = CLIMATE_DISCOVERY.format(base_topic, self._frn or name, self._composeAvailability(),
sys_vars.getDeviceID(), name, # unique_id
_mqtt.getRealTopic(
self.temp_sensor.getTopic(SENSOR_TEMPERATURE)),
# current_temp_topic
self.temp_sensor.getTemplate(SENSOR_TEMPERATURE),
# cur_temp_template
self._temp_step, self._min_temp, self._max_temp,
modes, sys_vars.getDeviceDiscovery())
if register:
sens = CLIMATE_DISCOVERY.format(base_topic, self._frn or name,
self._composeAvailability(),
sys_vars.getDeviceID(), name, # unique_id
_mqtt.getRealTopic(
self.temp_sensor.getTopic(SENSOR_TEMPERATURE)),
# current_temp_topic
self.temp_sensor.getTemplate(SENSOR_TEMPERATURE),
# cur_temp_template
self._temp_step, self._min_temp, self._max_temp,
modes, sys_vars.getDeviceDiscovery())
else:
sens = ""
gc.collect()
topic = Component._getDiscoveryTopic(_COMPONENT_TYPE, name)
await _mqtt.publish(topic, sens, qos=1, retain=True)


class BaseMode:
"""
Base class for all modes
"""

def __init__(self, climate: Climate):
pass

async def trigger(self, climate: Climate, current_temp: float) -> bool:
"""Triggered whenever the situation is evaluated again"""
raise NotImplementedError

async def activate(self, climate: Climate) -> bool:
"""Triggered whenever the mode changes and this mode has been activated"""
raise NotImplementedError

async def deactivate(self, climate: Climate) -> bool:
"""Triggered whenever the mode changes and this mode has been deactivated"""
raise NotImplementedError

def __str__(self):
"""Name of the mode, has to be the same as the classname/module"""
raise NotImplementedError
10 changes: 5 additions & 5 deletions pysmartnode/components/devices/climate/heat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# Copyright Kevin Köck 2019 Released under the MIT license
# Created on 2019-10-12

__updated__ = "2019-10-28"
__updated__ = "2019-10-31"
__version__ = "0.4"

from pysmartnode.components.devices.climate import BaseMode
from pysmartnode.components.devices.climate import BaseMode, Climate
from .definitions import ACTION_HEATING, ACTION_IDLE, MODE_HEAT, CURRENT_ACTION, \
CURRENT_TEMPERATURE_HIGH, CURRENT_TEMPERATURE_LOW

Expand All @@ -15,7 +15,7 @@ def __init__(self, climate):
super().__init__(climate)
self._last_state = False

async def trigger(self, climate, current_temp):
async def trigger(self, climate: Climate, current_temp: float) -> bool:
"""Triggered whenever the situation is evaluated again"""
if current_temp is None:
await climate.log.asyncLog("warn", "No temperature", timeout=2, await_connection=False)
Expand Down Expand Up @@ -54,12 +54,12 @@ async def trigger(self, climate, current_temp):
climate.state[CURRENT_ACTION] = ACTION_IDLE
self._last_state = False

async def activate(self, climate):
async def activate(self, climate: Climate) -> bool:
"""Triggered whenever the mode changes and this mode has been activated"""
self._last_state = climate.heating_unit.state()
return True

async def deactivate(self, climate):
async def deactivate(self, climate: Climate) -> bool:
"""Triggered whenever the mode changes and this mode has been deactivated"""
return True # no deinit needed

Expand Down
10 changes: 5 additions & 5 deletions pysmartnode/components/devices/climate/off.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
# Copyright Kevin Köck 2019 Released under the MIT license
# Created on 2019-10-12

__updated__ = "2019-10-28"
__updated__ = "2019-10-31"
__version__ = "0.2"

from pysmartnode.components.devices.climate import BaseMode
from pysmartnode.components.devices.climate import BaseMode, Climate
from .definitions import ACTION_OFF, MODE_OFF, CURRENT_ACTION


class off(BaseMode):
# def __init__(self, climate):

async def trigger(self, climate, current_temp):
async def trigger(self, climate: Climate, current_temp: float) -> bool:
"""Triggered whenever the situation is evaluated again"""
if climate.heating_unit.state() is False and climate.state[CURRENT_ACTION] == ACTION_OFF:
return True
Expand All @@ -21,11 +21,11 @@ async def trigger(self, climate, current_temp):
return True
return False

async def activate(self, climate):
async def activate(self, climate: Climate) -> bool:
"""Triggered whenever the mode changes and this mode has been activated"""
return True # no init needed

async def deactivate(self, climate):
async def deactivate(self, climate: Climate) -> bool:
"""Triggered whenever the mode changes and this mode has been deactivated"""
return True # no deinit needed

Expand Down
Loading

0 comments on commit 61e6987

Please sign in to comment.