This commit is contained in:
Nils Schulte 2021-01-17 22:41:51 +01:00
parent b8549ac286
commit 9fce8b9e56
6 changed files with 610 additions and 0 deletions

42
code/bmp280_node.py Normal file
View File

@ -0,0 +1,42 @@
from homie.constants import FLOAT
from homie.property import HomieProperty
from update_homie_node import UpdateHomieNode
# from homie.device import await_ready_state
# import uasyncio as asyncio
# from time import ticks_ms, ticks_add, ticks_diff
class BMP280Node(UpdateHomieNode):
def __init__(
self,
id,
name,
bmp280,
interval=60*5):
super().__init__(id=id, name=name, type="sensor", interval=interval)
# BMP280
self.bmp280 = bmp280
self.property_temerature = HomieProperty(
id="temperature",
name="Temperatur",
datatype=FLOAT,
unit="°C",
)
self.add_property(self.property_temerature)
self.property_pressure = HomieProperty(
id="pressure",
name="Druck",
datatype=FLOAT,
unit="Pa",
)
self.add_property(self.property_pressure)
def update_data(self):
self.property_pressure.value = "{:1.0f}".format(
self.bmp280.pressure * 100) # hPa = 100 Pa
self.property_temerature.value = "{:1.2f}".format(
self.bmp280.temperature)

114
code/lib/rotary.py Normal file
View File

@ -0,0 +1,114 @@
# The MIT License (MIT)
# Copyright (c) 2020 Mike Teachman
# https://opensource.org/licenses/MIT
# Platform-independent MicroPython code for the rotary encoder module
# Documentation:
# https://github.com/MikeTeachman/micropython-rotary
_DIR_CW = const(0x10) # Clockwise step
_DIR_CCW = const(0x20) # Counter-clockwise step
# Rotary Encoder States
_R_START = const(0x0)
_R_CW_1 = const(0x1)
_R_CW_2 = const(0x2)
_R_CW_3 = const(0x3)
_R_CCW_1 = const(0x4)
_R_CCW_2 = const(0x5)
_R_CCW_3 = const(0x6)
_R_ILLEGAL = const(0x7)
_transition_table = [
#|------------- NEXT STATE -------------| |CURRENT STATE|
# CLK/DT CLK/DT CLK/DT CLK/DT
# 00 01 10 11
[_R_START, _R_CCW_1, _R_CW_1, _R_START], # _R_START
[_R_CW_2, _R_START, _R_CW_1, _R_START], # _R_CW_1
[_R_CW_2, _R_CW_3, _R_CW_1, _R_START], # _R_CW_2
[_R_CW_2, _R_CW_3, _R_START, _R_START | _DIR_CW], # _R_CW_3
[_R_CCW_2, _R_CCW_1, _R_START, _R_START], # _R_CCW_1
[_R_CCW_2, _R_CCW_1, _R_CCW_3, _R_START], # _R_CCW_2
[_R_CCW_2, _R_START, _R_CCW_3, _R_START | _DIR_CCW], # _R_CCW_3
[_R_START, _R_START, _R_START, _R_START]] # _R_ILLEGAL
_STATE_MASK = const(0x07)
_DIR_MASK = const(0x30)
def _wrap(value, incr, lower_bound, upper_bound):
range = upper_bound - lower_bound + 1
value = value + incr
if value < lower_bound:
value += range * ((lower_bound - value) // range + 1)
return lower_bound + (value - lower_bound) % range
def _bound(value, incr, lower_bound, upper_bound):
return min(upper_bound, max(lower_bound, value + incr))
class Rotary(object):
RANGE_UNBOUNDED = const(1)
RANGE_WRAP = const(2)
RANGE_BOUNDED = const(3)
def __init__(self, min_val, max_val, reverse, range_mode):
self._min_val = min_val
self._max_val = max_val
self._reverse = -1 if reverse else 1
self._range_mode = range_mode
self._value = min_val
self._state = _R_START
def set(self, value=None, min_val=None, max_val=None, reverse=None, range_mode=None):
# disable DT and CLK pin interrupts
self._hal_disable_irq()
if value != None:
self._value = value
if min_val != None:
self._min_val = min_val
if max_val != None:
self._max_val = max_val
if reverse != None:
self._reverse = -1 if reverse else 1
if range_mode != None:
self._range_mode = range_mode
self._state = _R_START
# enable DT and CLK pin interrupts
self._hal_enable_irq()
def value(self):
return self._value
def reset(self):
self._value = 0
def close(self):
self._hal_close()
def _process_rotary_pins(self, pin):
clk_dt_pins = (self._hal_get_clk_value() << 1) | self._hal_get_dt_value()
# Determine next state
self._state = _transition_table[self._state & _STATE_MASK][clk_dt_pins]
direction = self._state & _DIR_MASK
incr = 0
if direction == _DIR_CW:
incr = 1
elif direction == _DIR_CCW:
incr = -1
incr *= self._reverse
if self._range_mode == self.RANGE_WRAP:
self._value = _wrap(self._value, incr, self._min_val, self._max_val)
elif self._range_mode == self.RANGE_BOUNDED:
self._value = _bound(self._value, incr, self._min_val, self._max_val)
else:
self._value = self._value + incr

View File

@ -0,0 +1,68 @@
# The MIT License (MIT)
# Copyright (c) 2020 Mike Teachman
# https://opensource.org/licenses/MIT
# Platform-specific MicroPython code for the rotary encoder module
# ESP8266/ESP32 implementation
# Documentation:
# https://github.com/MikeTeachman/micropython-rotary
from machine import Pin
from rotary import Rotary
from sys import platform
_esp8266_deny_pins = [16]
class RotaryIRQ(Rotary):
def __init__(self, pin_num_clk, pin_num_dt, min_val=0, max_val=10, reverse=False, range_mode=Rotary.RANGE_UNBOUNDED, pull_up=False):
if platform == 'esp8266':
if pin_num_clk in _esp8266_deny_pins:
raise ValueError('%s: Pin %d not allowed. Not Available for Interrupt: %s' % (platform, pin_num_clk,_esp8266_deny_pins))
if pin_num_dt in _esp8266_deny_pins:
raise ValueError('%s: Pin %d not allowed. Not Available for Interrupt: %s' % (platform, pin_num_dt,_esp8266_deny_pins))
super().__init__(min_val, max_val, reverse, range_mode)
if pull_up == True:
self._pin_clk = Pin(pin_num_clk, Pin.IN, Pin.PULL_UP)
self._pin_dt = Pin(pin_num_dt, Pin.IN, Pin.PULL_UP)
else:
self._pin_clk = Pin(pin_num_clk, Pin.IN)
self._pin_dt = Pin(pin_num_dt, Pin.IN)
self._enable_clk_irq(self._process_rotary_pins)
self._enable_dt_irq(self._process_rotary_pins)
def _enable_clk_irq(self, callback=None):
self._pin_clk.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=callback)
def _enable_dt_irq(self, callback=None):
self._pin_dt.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=callback)
def _disable_clk_irq(self):
self._pin_clk.irq(handler=None)
def _disable_dt_irq(self):
self._pin_dt.irq(handler=None)
def _hal_get_clk_value(self):
return self._pin_clk.value()
def _hal_get_dt_value(self):
return self._pin_dt.value()
def _hal_enable_irq(self):
self._enable_clk_irq(self._process_rotary_pins)
self._enable_dt_irq(self._process_rotary_pins)
def _hal_disable_irq(self):
self._disable_clk_irq()
self._disable_dt_irq()
def _hal_close(self):
self._hal_disable_irq()

View File

@ -0,0 +1,179 @@
from sh1106 import SH1106_I2C
from rotary_irq_esp import RotaryIRQ
import uasyncio as asyncio
from time import ticks_ms, ticks_add, ticks_diff
from homie.constants import BOOLEAN, TRUE, FALSE, FLOAT
from homie.property import HomieProperty
from homie.node import HomieNode
from homie.device import await_ready_state
from machine import Pin
from primitives.pushbutton import Pushbutton
class PlantBoxControlPanel(HomieNode):
def __init__(self, id, name, waterlevel_sensor, bmp280,
i2c, pin_clk, pin_dt, pin_sw):
super().__init__(id=id, name=name, type="controller")
self.i2c = i2c
self.bmp280 = bmp280
self.display = SH1106_I2C(
i2c=self.i2c, width=128, height=64)
self.display.rotate(True, False)
self.rotary_encoder = RotaryIRQ(
pin_num_clk=int(str(pin_clk)[4:][:-1]),
pin_num_dt=int(str(pin_dt)[4:][:-1]),
min_val=0,
max_val=3,
reverse=False,
pull_up=True,
range_mode=RotaryIRQ.RANGE_WRAP)
self.waterlevel_sensor = waterlevel_sensor
pin_sw.init(mode=Pin.IN, pull=Pin.PULL_UP)
self.button = Pushbutton(pin_sw, suppress=True, sense=1)
self._screen_on = False
self.needs_redraw = False
self.update_interval = 0.3
self.property_button_pressed = HomieProperty(
id="button_pressed",
name="Knöpgen gedrückt",
settable=True,
default=FALSE,
on_message=self._on_button_pressed_msg,
datatype=BOOLEAN,
)
self.add_property(self.property_button_pressed)
self.property_screen_on = HomieProperty(
id="screen_on",
name="Bildschrim angeschaltet",
settable=True,
default=FALSE,
on_message=self.on_screen_on_msg,
datatype=BOOLEAN,
)
self.add_property(self.property_screen_on)
self.screen_timeout = 30
self.screen_timeout_ticks = 0
self.property_screen_timeout = HomieProperty(
id="screen_timeout",
name="Bildschrimtimeout",
settable=True,
default=str(self.screen_timeout),
on_message=self.on_screen_timeout_msg,
datatype=FLOAT,
)
self.add_property(self.property_screen_timeout)
self.on_button_pressed = None
self.on_button_released = None
asyncio.create_task(self._update_data_async())
def _on_button_pressed_msg(self, topic, payload, retained):
if {FALSE: False, TRUE: True}[payload] and not self.button.rawstate():
self._on_button_pressed()
self._on_button_released()
def _on_button_pressed(self):
self.property_button_pressed.value = TRUE
self.needs_redraw = True
if self.on_button_pressed:
self.on_button_pressed()
def _on_button_released(self):
self.property_button_pressed.value = FALSE
self._set_screen_on(True)
self.needs_redraw = True
if self.on_button_released:
self.on_button_released()
def on_screen_on_msg(self, topic, payload, retained):
self._set_screen_on({FALSE: False, TRUE: True}[payload])
def on_screen_timeout_msg(self, topic, payload, retained):
new_screen_timeout = float(payload)
self.screen_timeout_ticks = \
ticks_add(
ticks_diff(
self.screen_timeout_ticks,
int(self.screen_timeout * 1000)),
int(new_screen_timeout * 1000))
self.screen_timeout = new_screen_timeout
def reset_screen_timout(self):
self.screen_timeout_ticks = ticks_add(
ticks_ms(),
int(self.screen_timeout * 1000))
# @await_ready_state
async def _update_data_async(self):
self.button.press_func(self._on_button_pressed)
self.button.release_func(self._on_button_released)
rotary_last = 0
last_temp = 0
while True:
if ticks_diff(self.screen_timeout_ticks, ticks_ms()) <= 0:
self._set_screen_on(False)
if self._screen_on:
if rotary_last != self.rotary_encoder.value():
rotary_last = self.rotary_encoder.value()
self.needs_redraw = True
if last_temp != self.bmp280.temperature:
self.needs_redraw = True
last_temp = self.bmp280.temperature
if self.needs_redraw:
self.needs_redraw = False
# self.display.fill(0)
# self.display.text(str(self.rotary_encoder.value()), 0, 20)
# self.display.text(str(self.button.rawstate()), 0, 30)
self.display.fill_rect(0, 0, 128, 10, 0)
self.display.text("{:1.2f}°C".format(self.bmp280.temperature), 1, 1)
actions = [("Einst.", None), ("Wasser", lambda : self._set_screen_on(False))]
self.rotary_encoder.set(max_val=len(actions) - 1)
for i, (a, br) in enumerate(actions):
selected = i == self.rotary_encoder.value()
color = 0 if selected else 1
screen_dif = int(128 / len(actions))
self.display.fill_rect(
screen_dif * i+1, 50, screen_dif-2, 10, 1-color)
self.display.text(a, screen_dif*i+2, 51, color)
if not self.screen_just_turned_on:
if selected:
self.on_button_released = br
self.display.show()
self.screen_just_turned_on = False
else:
self.rotary_encoder.set(value=0)
self.on_button_released, self.on_button_released = None, None
await asyncio.sleep_ms(int(self.update_interval*1000.0))
@property
def screen_on(self):
return self._screen_on
@screen_on.setter
def set_screen_on(self, on):
self._set_screen_on(on)
def _set_screen_on(self, on):
if on == self._screen_on:
if on:
self.reset_screen_timout()
else:
self._screen_on = on
if on:
# self.display.poweron()
self.reset_screen_timout()
self.needs_redraw = True
self.screen_just_turned_on = True
else:
self.display.poweroff()
self.property_screen_on = TRUE if on else FALSE

131
code/plant_node.py Normal file
View File

@ -0,0 +1,131 @@
from homie.constants import BOOLEAN, FALSE, TRUE, FLOAT
from homie.property import HomieProperty
from update_homie_node import UpdateHomieNode
# from homie.device import await_ready_state
from machine import Pin, ADC
import uasyncio as asyncio
class PlantNode(UpdateHomieNode):
def __init__(
self,
id,
name,
pin_watering,
pin_moisture,
pin_water_tank,
waterlevel_sensor,
interval=60*5,
interval_watering=0.1):
super().__init__(
id=id, name=name, type="watering",
interval=interval,
interval_short=interval_watering)
# Update Interval
self.interval_normal = interval
self.interval_watering = interval_watering
# WaterLevelSensor
self.waterlevel_sensor = waterlevel_sensor
self.property_waterlevel = HomieProperty(
id="waterlevel",
name="Wassertankstand",
datatype=FLOAT,
unit="L",
)
self.add_property(self.property_waterlevel)
self.property_waterlevel_percent = HomieProperty(
id="waterlevel_percent",
name="Wassertankstand [%]",
datatype=FLOAT,
format="0.00:100.00",
unit="%",
)
self.add_property(self.property_waterlevel_percent)
self.property_waterlevel_volume_liter = HomieProperty(
id="waterlevel_volume_max",
name="Wassertankgröße",
settable=True,
datatype=FLOAT,
unit="L",
on_message=self._set_waterlevel_volume
)
self.add_property(self.property_waterlevel_volume_liter)
# Moisture
self.adc = ADC(pin_moisture)
self.adc.atten(ADC.ATTN_11DB)
self.property_moisture = HomieProperty(
id="moisture",
name="Feuchte",
datatype=FLOAT,
format="0.00:100.00",
unit="%",
)
self.add_property(self.property_moisture)
# Watering Motor
self.pin_watering_motor = pin_watering
self.pin_watering_motor.init(mode=Pin.OUT, value=0)
self.property_water_power = HomieProperty(
id="power",
name="Bewässerung",
settable=True,
datatype=BOOLEAN,
default=FALSE,
on_message=self.toggle_motor,
)
self.add_property(self.property_water_power)
self.property_watering_max_duration = HomieProperty(
id="watering_duration_max",
name="Bewässerungszeit",
settable=True,
datatype=FLOAT,
default=3,
unit="s",
)
self.add_property(self.property_watering_max_duration)
def update_data(self):
self.property_moisture.value = "{:1.2f}".format(
(4096 - self.adc.read()) / 40.96)
self.property_waterlevel_percent.value = "{:1.0f}".format(
self.waterlevel_sensor.level_percent)
self.property_waterlevel.value = "{:1.2f}".format(
self.waterlevel_sensor.level)
self.property_waterlevel_volume_liter.value = "{:1.4f}".format(
self.waterlevel_sensor.volume)
if self.pin_watering_motor.value() == 1:
self.interval_normal = self._interval
self.set_interval(self.interval_watering)
def toggle_motor(self, topic, payload, retained):
ONOFF = {FALSE: 0, TRUE: 1}
v = ONOFF[payload]
self.pin_watering_motor(v)
if v == 1:
self.interval_normal = self._interval
self.set_interval(self.interval_watering)
asyncio.create_task(self.stop_motor())
def stop_motor(self):
await asyncio.sleep_ms(
int(float(self.property_watering_max_duration.value) * 1000))
self.pin_watering_motor.value(0)
self.set_interval(self.interval_normal)
self.property_water_power.value = FALSE
def _set_waterlevel_min_value(self, topic, payload, retained):
self.waterlevel_sensor.value_min = float(payload)
def _set_waterlevel_max_value(self, topic, payload, retained):
self.waterlevel_sensor.value_max = float(payload)
def _set_waterlevel_volume(self, topic, payload, retained):
self.waterlevel_sensor.volume = float(payload)

76
code/update_homie_node.py Normal file
View File

@ -0,0 +1,76 @@
from homie.constants import FLOAT
from homie.property import HomieProperty
from homie.node import HomieNode
from homie.device import await_ready_state
import uasyncio as asyncio
from time import ticks_ms, ticks_add, ticks_diff
class UpdateHomieNode(HomieNode):
def __init__(
self,
id,
name,
type,
interval=60*5,
interval_short=0.1):
super().__init__(id=id, name=name, type=type)
self.interval_changed = False
self.interval_short = interval_short
# Update Interval
self._interval = interval
self.property_interval = HomieProperty(
id="update_interval",
name="Aktualisierungsrate",
datatype=FLOAT, # TODO ISO8601
settable=True,
on_message=self._set_interval,
unit="s",
)
self.add_property(self.property_interval)
asyncio.create_task(self._update_data_async())
@await_ready_state
async def _update_data_async(self):
while True:
# call child callback
self.update_data()
# TODO ISO8601
# self.property_interval.value = "PT{:1.3f}S".format(self.interval)
self.property_interval.value = "{:1.3f}".format(self.interval)
# We don't simply wait the update interval, as it can change while waiting.
last_update = ticks_ms()
wait_till = ticks_add(last_update, int(self.interval * 1000.0))
while ticks_diff(ticks_ms(), wait_till) < 0:
if self.interval_changed:
self.interval_changed = False
wait_till = ticks_add(
last_update,
int(self.interval * 1000))
sleep_for = min(int(self.interval_short * 1000.0),
ticks_diff(wait_till, ticks_ms()))
await asyncio.sleep_ms(sleep_for)
@property
def interval(self):
return self._interval
@interval.setter
def setter_interval(self, i):
self.set_interval(i)
def set_interval(self, i):
if i != self._interval:
self.interval_changed = True
self._interval = float(i)
def _set_interval(self, topic, payload, retained):
self.set_interval(float(payload))