Skip to content
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

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
Flavio Fernandes <[email protected]>
Copy link
Owner

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

Flavio Fernandes <[email protected]>
Norman Rasmussen <[email protected]>
Your Name <[email protected]>
20 changes: 20 additions & 0 deletions ChangeLog
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
18 changes: 18 additions & 0 deletions Dockerfile
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"]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, please add end of line

Empty file added build/lib/mqtt2kasa/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions build/lib/mqtt2kasa/bin/create-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
Copy link
Owner

Choose a reason for hiding this comment

The 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
11 changes: 11 additions & 0 deletions build/lib/mqtt2kasa/bin/mqtt2kasa.service.rpi
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
11 changes: 11 additions & 0 deletions build/lib/mqtt2kasa/bin/mqtt2kasa.service.vagrant
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
2 changes: 2 additions & 0 deletions build/lib/mqtt2kasa/bin/reload_config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
sudo systemctl restart mqtt2kasa
16 changes: 16 additions & 0 deletions build/lib/mqtt2kasa/bin/start_mqtt2kasa.sh
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
7 changes: 7 additions & 0 deletions build/lib/mqtt2kasa/bin/tail_log.sh
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
174 changes: 174 additions & 0 deletions build/lib/mqtt2kasa/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env python
Copy link
Owner

@flavio-fernandes flavio-fernandes Apr 6, 2024

Choose a reason for hiding this comment

The 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))
9 changes: 9 additions & 0 deletions build/lib/mqtt2kasa/const.py
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]
46 changes: 46 additions & 0 deletions build/lib/mqtt2kasa/events.py
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)
Loading