Initial commit, first working version
This commit is contained in:
commit
1531708338
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal 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
18
README.md
Normal 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
4
pyproject.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
13
setup.cfg
Normal file
13
setup.cfg
Normal 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
10
setup.py
Normal 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'
|
||||
]
|
||||
)
|
||||
|
140
src/citadel/scene/__init__.py
Normal file
140
src/citadel/scene/__init__.py
Normal 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', ''),
|
||||
]
|
44
src/citadel/scene/__main__.py
Normal file
44
src/citadel/scene/__main__.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user