mqtt wrapper
This commit is contained in:
parent
ac125cb4bd
commit
0982a213dd
137
door.py
137
door.py
|
@ -28,6 +28,43 @@ config = parser.parse_args()
|
||||||
def timestamp():
|
def timestamp():
|
||||||
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
class MqttValue:
|
||||||
|
def __init__(self, client, topic, persistent = False, start_value = None, *, translate = None, max_update = 0.1):
|
||||||
|
self.client = client
|
||||||
|
self.topic = topic
|
||||||
|
self.persistent = persistent
|
||||||
|
self.value = start_value
|
||||||
|
self.last_update = datetime.datetime.now() - datetime.timedelta(seconds = max_update)
|
||||||
|
self.last_update_value = None
|
||||||
|
self.max_update = max_update
|
||||||
|
|
||||||
|
if translate == None:
|
||||||
|
self.translate = str
|
||||||
|
elif callable(translate):
|
||||||
|
self.translate = translate
|
||||||
|
else:
|
||||||
|
self.translate = lambda x: translate[x]
|
||||||
|
|
||||||
|
if start_value != None:
|
||||||
|
self.update(start_value)
|
||||||
|
|
||||||
|
def update(self, value, force = False, no_update = False):
|
||||||
|
if value != self.value or value != self.last_update_value:
|
||||||
|
self.value = value
|
||||||
|
if force or (not no_update and value != self.last_update_value and (datetime.datetime.now() - self.last_update).total_seconds() >= self.max_update):
|
||||||
|
self.last_update = datetime.datetime.now()
|
||||||
|
self.last_update_value = value
|
||||||
|
self.client.publish(self.topic, self.translate(value), qos = 2, retain = self.persistent)
|
||||||
|
|
||||||
|
def force_update(self, value):
|
||||||
|
self.update(value, force = True)
|
||||||
|
|
||||||
|
def __call__(self, value = None, **kwargs):
|
||||||
|
if value != None:
|
||||||
|
self.update(value, **kwargs)
|
||||||
|
else:
|
||||||
|
return self.value
|
||||||
|
|
||||||
class DoorControl:
|
class DoorControl:
|
||||||
# Actions
|
# Actions
|
||||||
IDLE, CLOSE, OPEN_THEN_CLOSE, OPEN, CLOSE_THEN_OPEN, ERROR = range(6)
|
IDLE, CLOSE, OPEN_THEN_CLOSE, OPEN, CLOSE_THEN_OPEN, ERROR = range(6)
|
||||||
|
@ -60,10 +97,13 @@ class DoorControl:
|
||||||
self.mqttc.loop_start()
|
self.mqttc.loop_start()
|
||||||
|
|
||||||
# Current door state
|
# Current door state
|
||||||
self.state = None
|
self.state = MqttValue(self.mqttc, "door/state/value", True, translate = self.state_names)
|
||||||
self.state_pos = 0
|
self.state_target = MqttValue(self.mqttc, "door/state/target", True, translate = self.state_names, max_update = 0)
|
||||||
|
self.state_pos = MqttValue(self.mqttc, "door/position/value", True, 0)
|
||||||
|
self.last_invalid_token = MqttValue(self.mqttc, "door/token/last_invalid", True)
|
||||||
|
self.speed = MqttValue(self.mqttc, "door/position/speed")
|
||||||
# Current target action
|
# Current target action
|
||||||
self.action = DoorControl.CLOSE
|
self.action = MqttValue(self.mqttc, "door/state/action", True, DoorControl.CLOSE, translate = self.action_names, max_update = 0)
|
||||||
# Start time of the current action
|
# Start time of the current action
|
||||||
self.start_time = None
|
self.start_time = None
|
||||||
# How often the action was repeated
|
# How often the action was repeated
|
||||||
|
@ -104,10 +144,6 @@ class DoorControl:
|
||||||
|
|
||||||
return logger
|
return logger
|
||||||
|
|
||||||
def mqtt(self, topic: str, msg: str, persistent: bool = True):
|
|
||||||
"""Publishes data to a topic"""
|
|
||||||
self.mqttc.publish("door/" + topic, msg, qos=2, retain=persistent)
|
|
||||||
|
|
||||||
def _open_control_socket(self, config):
|
def _open_control_socket(self, config):
|
||||||
"""(Re-)creates and opens the control socket. Config must have a control_socket member."""
|
"""(Re-)creates and opens the control socket. Config must have a control_socket member."""
|
||||||
self.logger.debug("Opening control socket")
|
self.logger.debug("Opening control socket")
|
||||||
|
@ -127,7 +163,7 @@ class DoorControl:
|
||||||
self.serial_port = serial.Serial(config.serial_port, timeout=2)
|
self.serial_port = serial.Serial(config.serial_port, timeout=2)
|
||||||
self._send_door_cmd(b'r')
|
self._send_door_cmd(b'r')
|
||||||
except:
|
except:
|
||||||
serial_port = None
|
self.serial_port = None
|
||||||
return self.serial_port
|
return self.serial_port
|
||||||
|
|
||||||
def _open_nfc_fifo(self, config):
|
def _open_nfc_fifo(self, config):
|
||||||
|
@ -169,18 +205,14 @@ class DoorControl:
|
||||||
|
|
||||||
def _set_position(self, data: int):
|
def _set_position(self, data: int):
|
||||||
"""Set a new door position"""
|
"""Set a new door position"""
|
||||||
if self.state_pos != data:
|
self.state_pos(data)
|
||||||
self.mqtt("position/value", data, True)
|
if data > ERROR_THRESHOLD and self.state() != DoorControl.ERROR:
|
||||||
self.state_pos = data
|
|
||||||
if data > ERROR_THRESHOLD and self.state != DoorControl.ERROR:
|
|
||||||
self.logger.error("Invalid position:", state)
|
self.logger.error("Invalid position:", state)
|
||||||
self.state = DoorControl.ERROR
|
self.state(DoorControl.ERROR)
|
||||||
elif data > OPEN_THRESHOLD and self.state != DoorControl.OPEN:
|
elif data > OPEN_THRESHOLD and self.state() != DoorControl.OPEN:
|
||||||
self.mqtt("state/value", "open", True)
|
self.state(DoorControl.OPEN)
|
||||||
self.state = DoorControl.OPEN
|
elif data < CLOSED_THRESHOLD and self.state() != DoorControl.CLOSE:
|
||||||
elif data < CLOSED_THRESHOLD and self.state != DoorControl.CLOSE:
|
self.state(DoorControl.CLOSE)
|
||||||
self.mqtt("state/value", "closed", True)
|
|
||||||
self.state = DoorControl.CLOSE
|
|
||||||
|
|
||||||
def _check_reporting(self, current: int):
|
def _check_reporting(self, current: int):
|
||||||
if current == 0:
|
if current == 0:
|
||||||
|
@ -213,7 +245,7 @@ class DoorControl:
|
||||||
self.last_door_pos_time = datetime.datetime.now()
|
self.last_door_pos_time = datetime.datetime.now()
|
||||||
self._send_door_cmd(b"R")
|
self._send_door_cmd(b"R")
|
||||||
self.handle_door_line()
|
self.handle_door_line()
|
||||||
return self.state
|
return self.state()
|
||||||
|
|
||||||
def handle_door_state(self):
|
def handle_door_state(self):
|
||||||
"""Checks the door state and executes any actions necessary to reach the target state"""
|
"""Checks the door state and executes any actions necessary to reach the target state"""
|
||||||
|
@ -231,13 +263,6 @@ class DoorControl:
|
||||||
if 1. / delta_t > MAX_UPDATE_RATE:
|
if 1. / delta_t > MAX_UPDATE_RATE:
|
||||||
return
|
return
|
||||||
|
|
||||||
speed = abs(self.state_pos - self.last_position) / delta_t
|
|
||||||
self.last_position = self.state_pos
|
|
||||||
self.last_handle_state_timestamp = now
|
|
||||||
|
|
||||||
if speed >= 0 or delta_t >= 1. / UPDATE_RATE:
|
|
||||||
self.mqtt("position/speed", f"{speed}")
|
|
||||||
|
|
||||||
# If no serial port, try to open it or return
|
# If no serial port, try to open it or return
|
||||||
if not self.serial_port:
|
if not self.serial_port:
|
||||||
try:
|
try:
|
||||||
|
@ -247,21 +272,27 @@ class DoorControl:
|
||||||
|
|
||||||
# Get new state
|
# Get new state
|
||||||
self.poll_door_state()
|
self.poll_door_state()
|
||||||
self.last_handled_state = self.state
|
self.last_handled_state = self.state()
|
||||||
|
|
||||||
if self.state == DoorControl.ERROR:
|
speed = abs(self.state_pos() - self.last_position) / delta_t
|
||||||
|
speed = 10 * round(speed / 10)
|
||||||
|
self.speed(speed)
|
||||||
|
self.last_position = self.state_pos()
|
||||||
|
self.last_handle_state_timestamp = now
|
||||||
|
|
||||||
|
if self.state() == DoorControl.ERROR:
|
||||||
self.logger.error("Restarting the MCU and exiting.")
|
self.logger.error("Restarting the MCU and exiting.")
|
||||||
self.send_door_cmd(b'S')
|
self.send_door_cmd(b'S')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Idle + change = key?
|
# Idle + change = key?
|
||||||
if self.action == DoorControl.IDLE:
|
if self.action() == DoorControl.IDLE:
|
||||||
if self.state != old_state:
|
if self.state() != old_state:
|
||||||
self.logger.info(f"Door changed unexpectedly: {DoorControl.state_names[self.state]}")
|
self.logger.info(f"Door changed unexpectedly: {DoorControl.state_names[self.state()]}")
|
||||||
self.start_time = now
|
self.start_time = now
|
||||||
if self.start_time and (now - self.start_time).total_seconds() >= self.config.state_timeout:
|
if self.start_time and (now - self.start_time).total_seconds() >= self.config.state_timeout:
|
||||||
self.start_time = None
|
self.start_time = None
|
||||||
if self.state_pos <= CLOSED_THRESHOLD and self.state_pos > CLOSED_WANT:
|
if self.state_pos() <= CLOSED_THRESHOLD and self.state_pos() > CLOSED_WANT:
|
||||||
self.logger.info("Closing door a bit more")
|
self.logger.info("Closing door a bit more")
|
||||||
self._send_door_cmd(target_state_cmd[DoorControl.CLOSE])
|
self._send_door_cmd(target_state_cmd[DoorControl.CLOSE])
|
||||||
return
|
return
|
||||||
|
@ -276,50 +307,52 @@ class DoorControl:
|
||||||
|
|
||||||
if self.start_time is None:
|
if self.start_time is None:
|
||||||
self.start_time = now
|
self.start_time = now
|
||||||
self._send_door_cmd(target_state_cmd[actions[self.action][0]])
|
self._send_door_cmd(target_state_cmd[actions[self.action()][0]])
|
||||||
|
|
||||||
# Target state reached
|
# Target state reached
|
||||||
if self.state == actions[self.action][0]:
|
if self.state() == actions[self.action()][0]:
|
||||||
# Select next action, reset start time and repetitions
|
# Select next action, reset start time and repetitions
|
||||||
self.action = actions[self.action][1]
|
self.action(actions[self.action()][1])
|
||||||
self.start_time = now
|
self.start_time = now
|
||||||
self.repeats = 0
|
self.repeats = 0
|
||||||
# On idle, we're done
|
# On idle, we're done
|
||||||
if self.action == DoorControl.IDLE:
|
if self.action() == DoorControl.IDLE:
|
||||||
self.logger.debug(f"Reached target position: {DoorControl.state_names[self.state]}")
|
self.logger.debug(f"Reached target position: {DoorControl.state_names[self.state()]}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Execution time
|
# Execution time
|
||||||
t = (now - self.start_time).total_seconds()
|
t = (now - self.start_time).total_seconds()
|
||||||
if t >= self.config.state_timeout or (t >= self.config.state_timeout_speed and speed < MIN_SPEED and delta_t >= 1. / UPDATE_RATE):
|
if t >= self.config.state_timeout or (t >= self.config.state_timeout_speed and self.speed() < MIN_SPEED and delta_t >= 1. / UPDATE_RATE):
|
||||||
# Timeout -> switch to timeout action
|
# Timeout -> switch to timeout action
|
||||||
self.state = actions[self.action][0]
|
self.state(actions[self.action()][0], force = True, no_update = True)
|
||||||
self.action = actions[self.action][2]
|
self.action(actions[self.action()][2])
|
||||||
self.start_time = None
|
self.start_time = None
|
||||||
self.repeats = 0
|
self.repeats = 0
|
||||||
self.logger.debug(f"Timeout. Switching to {DoorControl.action_names[self.action]}")
|
self.logger.debug(f"Timeout. Switching to {DoorControl.action_names[self.action()]}")
|
||||||
elif t >= (1 + self.repeats) * self.config.repeat_time:
|
elif t >= (1 + self.repeats) * self.config.repeat_time:
|
||||||
# Repeat every couple of seconds
|
# Repeat every couple of seconds
|
||||||
self.repeats += 1
|
self.repeats += 1
|
||||||
self.logger.debug(f"Repeating command: {target_state_cmd[actions[self.action][0]]}")
|
self.logger.debug(f"Repeating command: {target_state_cmd[actions[self.action()][0]]}")
|
||||||
self._send_door_cmd(target_state_cmd[actions[self.action][0]])
|
self._send_door_cmd(target_state_cmd[actions[self.action()][0]])
|
||||||
|
|
||||||
def open_door(self):
|
def open_door(self):
|
||||||
self.logger.info("Opening the door")
|
self.logger.info("Opening the door")
|
||||||
self.mqtt("state/target", "open", True)
|
self.state_target(DoorControl.OPEN)
|
||||||
self.action = DoorControl.OPEN
|
self.action(DoorControl.OPEN)
|
||||||
self.start_time = None
|
self.start_time = None
|
||||||
self.handle_door_state()
|
self.handle_door_state()
|
||||||
|
|
||||||
def close_door(self):
|
def close_door(self):
|
||||||
self.logger.info("Closing the door")
|
self.logger.info("Closing the door")
|
||||||
self.mqtt("state/target", "closed", True)
|
self.state_target(DoorControl.CLOSE)
|
||||||
self.action = DoorControl.CLOSE
|
self.action(DoorControl.CLOSE)
|
||||||
self.start_time = None
|
self.start_time = None
|
||||||
self.handle_door_state()
|
self.handle_door_state()
|
||||||
|
|
||||||
def toggle_door_state(self):
|
def toggle_door_state(self):
|
||||||
if self.state == DoorControl.CLOSE:
|
if not self.action() == DoorControl.IDLE:
|
||||||
|
return
|
||||||
|
if self.state_target() == DoorControl.CLOSE:
|
||||||
self.open_door()
|
self.open_door()
|
||||||
else:
|
else:
|
||||||
self.close_door()
|
self.close_door()
|
||||||
|
@ -351,7 +384,7 @@ class DoorControl:
|
||||||
self.logger.warning(f"Token {token} of {data['name']} expired on {data['valid_thru']}")
|
self.logger.warning(f"Token {token} of {data['name']} expired on {data['valid_thru']}")
|
||||||
else:
|
else:
|
||||||
self.logger.warning(f"Invalid token: {token}")
|
self.logger.warning(f"Invalid token: {token}")
|
||||||
self.mqtt("token/last_invalid", "%s;%s" % (timestamp(), token))
|
self.last_invalid_token(f"{timestamp()};{token}")
|
||||||
|
|
||||||
class LineBuffer(object):
|
class LineBuffer(object):
|
||||||
def __init__(self, f, handler):
|
def __init__(self, f, handler):
|
||||||
|
@ -396,8 +429,8 @@ class DoorControl:
|
||||||
elif cmd == 'stat':
|
elif cmd == 'stat':
|
||||||
send("Door status is %s, position is %d. Current action: %s (%g seconds ago)\n" % (
|
send("Door status is %s, position is %d. Current action: %s (%g seconds ago)\n" % (
|
||||||
DoorControl.state_names.get(self.state, "None"),
|
DoorControl.state_names.get(self.state, "None"),
|
||||||
self.state_pos,
|
self.state_pos.value,
|
||||||
DoorControl.action_names[self.action],
|
DoorControl.action_names[self.action()],
|
||||||
(datetime.datetime.now() - self.start_time).total_seconds()))
|
(datetime.datetime.now() - self.start_time).total_seconds()))
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user