Initial commit, first working version

This commit is contained in:
Nicolas Duhamel 2021-03-18 13:58:50 +01:00
commit 1531708338
7 changed files with 237 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Source for the following rules: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.egg-info/

18
README.md Normal file
View File

@ -0,0 +1,18 @@
Citadel Scene
=============
A scene is a particular state of a part (or all) of your devices.
All is managed by mqtt.
Starting a scene is playing o bunch of mqtt payload.
Then watch for some specific payload that deactive the scene
A scene have a state ON/OFF
Topic
-----
stat/scene/id/state ON|OFF
cmnd/scene/id/activate ON|OFF

4
pyproject.toml Normal file
View File

@ -0,0 +1,4 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

13
setup.cfg Normal file
View File

@ -0,0 +1,13 @@
[metadata]
name = citadel.scene
version = 0.0.1
[options]
package_dir =
=src
packages = find_namespace:
# install_requires =
[options.packages.find]
where = src

10
setup.py Normal file
View File

@ -0,0 +1,10 @@
import setuptools
setuptools.setup(
install_requires = [
'citadel.mqtt @ git+https://git.quimbo.fr/citadel/mqtt.git@master#egg=citadel.mqtt',
'systemd-python',
'typer==0.3.2'
]
)

View File

@ -0,0 +1,140 @@
import threading
import logging
import paho.mqtt.client as mqtt
from citadel.mqtt import Configuration
class Scene(threading.Thread, mqtt.Client):
""" A scene run as a thread, has his own mqtt session
Can be activated by topic:
cmnd/scene/{mqtt_name}/activate ON|OFF|null
And return status on topic:
stat/scene/{mqtt_name}/state ON|OFF
Special class attributes:
mqtt_name: define topic name, default class name in lower case
"""
TOPIC_CMND = "cmnd/scene/{mqtt_name}/activate"
TOPIC_STAT = "stat/scene/{mqtt_name}/activate"
def __init__(self, config: Configuration, logger: logging.Logger=None):
threading.Thread.__init__(self)
mqtt.Client.__init__(self)
self.configuration = config
self._scene_state = False
if not hasattr(self, 'mqtt_name'):
self.mqtt_name = self.__class__.__name__.lower()
if not logger:
logger = logging.getLogger('scene.%s' % self.mqtt_name)
self.logger = logger
@property
def scene_state(self):
return self._scene_state
def run(self):
self.username_pw_set(self.configuration.user,
self.configuration.password)
self.connect(self.configuration.host,
self.configuration.port)
self.subscribe(self.TOPIC_CMND.format(mqtt_name=self.mqtt_name), 2)
for watch in self.WATCH:
self.subscribe(watch[0], 1)
run = True
while run:
self.loop()
def on_message(self, mqttc, obj, msg):
payload = msg.payload.decode().lower()
topic = msg.topic
# on CMND
if topic == self.TOPIC_CMND.format(mqtt_name=self.mqtt_name):
if payload == 'on':
self.logger.info('CMND activate ON')
self.__activate()
elif payload == 'off':
self.logger.info('CMND activate OFF')
self.__deactivate()
elif payload == '':
self.logger.info('CMND activate state')
self.__send_state()
else:
self.logger.warn('CMND activate invalide')
elif self.scene_state and self.__check_watched(topic, payload):
self.logger.info('Watched topic %s with invalid payload %s deactivating', topic, payload)
self.__deactivate()
def __check_watched(self, topic, payload):
""" return True if is a watched topic with an invalid payload """
for watch in self.WATCH:
if topic == watch[0]:
if payload != watch[1].lower():
return True
return False
return False
def __activate(self):
#activate scene
for action in self.ACTIVATE:
self.publish(action[0], payload=action[1], qos=2, retain=False)
self._scene_state = True
self.publish(self.TOPIC_STAT.format(mqtt_name=self.mqtt_name),
payload='ON',
qos=2,
retain=False)
def __deactivate(self):
#deactivate scene
self._scene_state = False
self.publish(self.TOPIC_STAT.format(mqtt_name=self.mqtt_name),
payload='OFF',
qos=2,
retain=False)
def __send_state(self):
print(self._scene_state)
self.publish(self.TOPIC_STAT.format(mqtt_name=self.mqtt_name),
payload='ON' if self.scene_state else 'OFF',
qos=2,
retain=False)
class Sleep(Scene):
ACTIVATE = [
# Close all Shutter
('cmnd/tasmota/shutter/Bedroom1x1/ShutterClose', ''),
('cmnd/tasmota/shutter/Bedroom1x2/ShutterClose', ''),
('cmnd/tasmota/shutter/Bathroom/ShutterClose', ''),
('cmnd/tasmota/shutter/Staicase1/ShutterClose', ''),
# Shutdown all light
# Set thermostat to sleepmode
# Close screen
('cmnd/tasmota/screen/Screen/POWER', 'OFF')
]
WATCH = [
('stat/tasmota/screen/Screen/POWER', 'OFF')
# ('cmnd/tasmota/shutter/Bedroom1x1/ShutterClose', ''),
# ('cmnd/tasmota/shutter/Bedroom1x2/ShutterClose', ''),
# ('cmnd/tasmota/shutter/Bathroom/ShutterClose', ''),
# ('cmnd/tasmota/shutter/Staicase1/ShutterClose', ''),
]

View File

@ -0,0 +1,44 @@
import logging
import typer
import systemd.journal
from citadel.mqtt import Configuration
from . import Sleep
def main(\
mqtt_user: str = typer.Option(... , envvar="DEVICES_MQTT_USER"),\
mqtt_pwd: str = typer.Option(... , envvar="DEVICES_MQTT_PWD"),\
mqtt_host: str = typer.Option(... , envvar="DEVICES_MQTT_HOST"),\
mqtt_port: int = typer.Option(... , envvar="DEVICES_MQTT_PORT"),\
is_systemd: bool = typer.Option(False, help="Is running as systemd unit", envvar="LAUNCHED_BY_SYSTEMD")\
):
if is_systemd:
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.addHandler(systemd.journal.JournalHandler())
else:
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# add formatter to ch
ch.setFormatter(formatter)
# add ch to logger
logger.addHandler(ch)
class Config:
user = mqtt_user
password = mqtt_pwd
host = mqtt_host
port = mqtt_port
scene = Sleep(Config)
scene.run()
if __name__ == "__main__":
typer.run(main)