-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue 14 Fix & Add Timestamps #15
Changes from all commits
abc2cc8
66ed18d
a0ac072
fe1897d
c7b4cb4
69213f7
efb111b
de9be49
c2ac14f
c8c2aaa
c23ea95
dc02b1f
0322e7f
fa46c99
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
Flavio Fernandes <[email protected]> | ||
Flavio Fernandes <[email protected]> | ||
Norman Rasmussen <[email protected]> | ||
Your Name <[email protected]> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,24 @@ | ||
CHANGES | ||
======= | ||
|
||
* aiomqtt: peg requirement to 1.x | ||
* aiomqtt: peg requirement to 1.2.1 or older | ||
* data/config.yaml.flaviof | ||
|
||
v0.0.2 | ||
------ | ||
|
||
* emeter: publish key and values as topics of emeter (#12) | ||
* logging: use sdtout as last resort (#11) | ||
* Add support for publishing emeter information | ||
|
||
v0.0.1 | ||
------ | ||
|
||
* config: add support for qos and retain in mqtt | ||
* add: kitchen-clock | ||
* Sync up with https://github.com/clmcavaney/mqtt2kasa | ||
* Make it python 3.7 compatible | ||
* Add toggle support and better tests | ||
* Trivial: New device to flaviof config and strictyaml to TODO | ||
* first commit |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
FROM python:3.13-rc-bookworm | ||
|
||
COPY . /src | ||
|
||
RUN apt-get update && \ | ||
apt-get install -y git curl build-essential gcc make | ||
|
||
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y | ||
|
||
ENV PATH="/root/.cargo/bin:${PATH}" | ||
|
||
RUN python3 -m pip install cffi | ||
RUN python3 -m pip install --no-cache-dir git+https://github.com/sbtinstruments/aiomqtt | ||
RUN python3 -m pip install --no-cache-dir -r /src/requirements.txt | ||
WORKDIR /src | ||
RUN python3 setup.py install | ||
|
||
ENTRYPOINT ["python3", "/src/mqtt2kasa/main.py", "/src/data/config.yaml"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit, please add end of line |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
#!/bin/bash | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm... I am hoping we could move the files here, instead of duplicating them. If that is intentional, then maybe we could consider using symbolic links. Let me thinks a bit more about this. |
||
set -o errexit | ||
set -o xtrace | ||
|
||
cd "$(dirname $0)" | ||
BIN_DIR="${PWD}" | ||
PROG_DIR="${BIN_DIR%/*}" | ||
TOP_DIR="${PROG_DIR%/*}" | ||
|
||
pushd ${TOP_DIR} | ||
if [ ! -e ./env ]; then | ||
#virtualenv --system-site-packages env | ||
python3 -m venv --copies env | ||
fi | ||
source ./env/bin/activate | ||
#pip install --upgrade pip setuptools | ||
pip install --ignore-installed -r ./requirements.txt | ||
deactivate | ||
|
||
popd | ||
exit 0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
[Unit] | ||
Description=MQTT front end wrapper to python-kasa | ||
|
||
[Service] | ||
User=pi | ||
Type=simple | ||
ExecStart=/home/pi/mqtt2kasa.git/mqtt2kasa/bin/start_mqtt2kasa.sh | ||
Restart=on-failure | ||
|
||
[Install] | ||
WantedBy=multi-user.target |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
[Unit] | ||
Description=MQTT front end wrapper to python-kasa | ||
|
||
[Service] | ||
User=vagrant | ||
Type=simple | ||
ExecStart=/vagrant/mqtt2kasa/bin/start_mqtt2kasa.sh /home/vagrant/mqtt2kasa.config.yaml | ||
Restart=on-failure | ||
|
||
[Install] | ||
WantedBy=multi-user.target |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
#!/bin/bash | ||
sudo systemctl restart mqtt2kasa |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
#!/bin/bash | ||
|
||
set -o errexit | ||
#set -x | ||
|
||
cd "$(dirname $0)" | ||
BIN_DIR="${PWD}" | ||
PROG_DIR="${BIN_DIR%/*}" | ||
TOP_DIR="${PROG_DIR%/*}" | ||
|
||
cd ${TOP_DIR}/env | ||
source ./bin/activate | ||
export PYTHONPATH=${PYTHONPATH:-$TOP_DIR} | ||
cd ${PROG_DIR} && ./main.py $@ | ||
|
||
exit 0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#!/bin/bash | ||
|
||
if [ -z "$1" ]; then | ||
sudo journalctl -u mqtt2kasa.service --no-pager --follow | ||
else | ||
sudo tail -F /var/log/syslog | grep mqtt2kasa | ||
fi |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
#!/usr/bin/env python | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so much duplication... is that intentional? |
||
import collections | ||
import os | ||
import sys | ||
from collections import namedtuple | ||
|
||
import yaml | ||
|
||
from mqtt2kasa import const | ||
from mqtt2kasa import log | ||
|
||
CFG_FILENAME = os.path.dirname(os.path.abspath(const.__file__)) + "/../data/config.yaml" | ||
Info = namedtuple("Info", "mqtt knobs cfg_globals locations keep_alives raw_cfg") | ||
|
||
|
||
class Cfg: | ||
_info = None # class (or static) variable | ||
|
||
def __init__(self): | ||
pass | ||
|
||
@property | ||
def mqtt_host(self): | ||
attr = self._get_info().mqtt | ||
if isinstance(attr, collections.abc.Mapping): | ||
return attr.get("host", const.MQTT_DEFAULT_BROKER_IP) | ||
return const.MQTT_DEFAULT_BROKER_IP | ||
|
||
@property | ||
def mqtt_client_id(self): | ||
attr = self._get_info().mqtt | ||
if isinstance(attr, collections.abc.Mapping): | ||
return attr.get("client_id", const.MQTT_DEFAULT_CLIENT_ID) | ||
return const.MQTT_DEFAULT_CLIENT_ID | ||
|
||
@property | ||
def mqtt_username(self): | ||
attr = self._get_info().mqtt | ||
if isinstance(attr, collections.abc.Mapping): | ||
return attr.get("username", None) | ||
return None | ||
|
||
@property | ||
def mqtt_password(self): | ||
attr = self._get_info().mqtt | ||
if isinstance(attr, collections.abc.Mapping): | ||
return attr.get("password", None) | ||
return None | ||
|
||
@property | ||
def mqtt_retain(self): | ||
attr = self._get_info().mqtt | ||
if isinstance(attr, collections.abc.Mapping): | ||
return attr.get("retain", False) | ||
return False | ||
|
||
@property | ||
def mqtt_qos(self): | ||
attr = self._get_info().mqtt | ||
if isinstance(attr, collections.abc.Mapping): | ||
return attr.get("qos", 0) | ||
return 0 | ||
|
||
@property | ||
def reconnect_interval(self): | ||
attr = self._get_info().mqtt | ||
if isinstance(attr, collections.abc.Mapping): | ||
return attr.get("reconnect_interval", const.MQTT_DEFAULT_RECONNECT_INTERVAL) | ||
return const.MQTT_DEFAULT_RECONNECT_INTERVAL | ||
|
||
@property | ||
def knobs(self): | ||
return self._get_info().knobs | ||
|
||
def mqtt_topic(self, location_name): | ||
locations = self._get_info().locations | ||
if isinstance(locations, collections.abc.Mapping): | ||
location_attributes = locations.get(location_name, {}) | ||
if location_attributes.get("topic"): | ||
return location_attributes["topic"].format(location_name) | ||
cfg_globals = self._get_info().cfg_globals | ||
topic_format = cfg_globals.get("topic_format") | ||
return ( | ||
topic_format.format(location_name) | ||
if topic_format | ||
else const.MQTT_DEFAULT_CLIENT_TOPIC_FORMAT.format(location_name) | ||
) | ||
|
||
@property | ||
def keep_alive_task_interval(self): | ||
cfg_globals = self._get_info().cfg_globals | ||
return float( | ||
cfg_globals.get("keep_alive_task_interval") | ||
or const.KEEP_ALIVE_DEFAULT_TASK_INTERVAL | ||
) | ||
|
||
def poll_interval(self, location_name): | ||
locations = self._get_info().locations | ||
if isinstance(locations, collections.abc.Mapping): | ||
location_attributes = locations.get(location_name, {}) | ||
if location_attributes.get("poll_interval"): | ||
return float(location_attributes["poll_interval"]) | ||
cfg_globals = self._get_info().cfg_globals | ||
return float( | ||
cfg_globals.get("poll_interval") or const.KASA_DEFAULT_POLL_INTERVAL | ||
) | ||
|
||
def emeter_poll_interval(self, location_name): | ||
locations = self._get_info().locations | ||
if isinstance(locations, collections.abc.Mapping): | ||
location_attributes = locations.get(location_name, {}) | ||
if location_attributes.get("emeter_poll_interval"): | ||
return float(location_attributes["emeter_poll_interval"]) | ||
cfg_globals = self._get_info().cfg_globals | ||
return float( | ||
cfg_globals.get("emeter_poll_interval") | ||
or const.KASA_DEFAULT_EMETER_POLL_INTERVAL | ||
) | ||
|
||
@property | ||
def locations(self): | ||
return self._get_info().locations | ||
|
||
@property | ||
def keep_alives(self): | ||
return self._get_info().keep_alives | ||
|
||
@classmethod | ||
def _get_config_filename(cls): | ||
if len(sys.argv) > 1: | ||
return sys.argv[1] | ||
return CFG_FILENAME | ||
|
||
@classmethod | ||
def _get_info(cls): | ||
if not cls._info: | ||
config_filename = cls._get_config_filename() | ||
logger.info("loading yaml config file %s", config_filename) | ||
with open(config_filename, "r") as ymlfile: | ||
raw_cfg = yaml.safe_load(ymlfile) | ||
cls._parse_raw_cfg(raw_cfg) | ||
return cls._info | ||
|
||
@classmethod | ||
def _parse_raw_cfg(cls, raw_cfg): | ||
cfg_globals = raw_cfg.get("globals", {}) | ||
assert isinstance(cfg_globals, dict) | ||
locations = raw_cfg.get("locations") | ||
assert isinstance(locations, dict) | ||
keep_alives = raw_cfg.get("keep_alives", {}) | ||
assert isinstance(keep_alives, dict) | ||
|
||
cls._info = Info( | ||
raw_cfg.get("mqtt"), | ||
raw_cfg.get("knobs", {}), | ||
cfg_globals, | ||
locations, | ||
keep_alives, | ||
raw_cfg, | ||
) | ||
|
||
|
||
# ============================================================================= | ||
|
||
|
||
logger = log.getLogger() | ||
if __name__ == "__main__": | ||
log.initLogger() | ||
c = Cfg() | ||
logger.info("c.knobs: {}".format(c.knobs)) | ||
logger.info("c.mqtt_host: {}".format(c.mqtt_host)) | ||
logger.info("c.cfg_globals: {}".format(c.cfg_globals)) | ||
logger.info("c.locations: {}".format(c.locations)) | ||
logger.info("c.keep_alives: {}".format(c.keep_alives)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
#!/usr/bin/env python | ||
|
||
MQTT_DEFAULT_CLIENT_ID = "mqtt2kasa" | ||
MQTT_DEFAULT_CLIENT_TOPIC_FORMAT = "/kasa/device/{}" | ||
MQTT_DEFAULT_BROKER_IP = "192.168.10.238" | ||
MQTT_DEFAULT_RECONNECT_INTERVAL = 13 # [seconds] | ||
KASA_DEFAULT_POLL_INTERVAL = 10 # [seconds] | ||
KASA_DEFAULT_EMETER_POLL_INTERVAL = 0 # [seconds] 0 == disabled | ||
KEEP_ALIVE_DEFAULT_TASK_INTERVAL = 1.5 # [seconds] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
#!/usr/bin/env python | ||
from collections import namedtuple | ||
|
||
|
||
class BaseEvent: | ||
def __init__(self, expected_attrs, attrs): | ||
self.event = self.__class__.__name__ | ||
self.attrs = self._dict_to_attrs(attrs) | ||
self._check_expected_attrs(expected_attrs) | ||
|
||
def __getattr__(self, attr): | ||
try: | ||
return getattr(self.attrs, attr) | ||
except AttributeError as e: | ||
raise AttributeError( | ||
f"{self.event} object is missing {attr} attribute" | ||
) from e | ||
|
||
def _check_expected_attrs(self, expected_attrs): | ||
if expected_attrs: | ||
for attr in expected_attrs: | ||
getattr(self, attr) | ||
|
||
@staticmethod | ||
def _dict_to_attrs(params_dict): | ||
cls = namedtuple("Attrs", params_dict) | ||
cls.__new__.__defaults__ = tuple(params_dict.values()) | ||
return cls() | ||
|
||
|
||
class MqttMsgEvent(BaseEvent): | ||
def __init__(self, **attrs): | ||
expected_attrs = "topic", "payload" | ||
super().__init__(expected_attrs, attrs) | ||
|
||
|
||
class KasaStateEvent(BaseEvent): | ||
def __init__(self, **attrs): | ||
expected_attrs = "name", "state" | ||
super().__init__(expected_attrs, attrs) | ||
|
||
|
||
class KasaEmeterEvent(BaseEvent): | ||
def __init__(self, **attrs): | ||
expected_attrs = "name", "emeter_status" | ||
super().__init__(expected_attrs, attrs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's remove lines 1 and 4