aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2023-09-24 02:59:41 +0300
committerEvgeny Zinoviev <me@ch1p.io>2023-09-24 02:59:41 +0300
commitbae3534f5a98c0c13fc7522d2c179a47ba685d98 (patch)
tree91efbbbee3f1d44c603645c6bd36dde3eebe461f
parent405a17a9fdd420faa7af90f769e72eb21fda73ce (diff)
parent3623e770b6b25fcaa1c8d76b9d3dafefec480876 (diff)
Merge branch 'master' into website-python-rewrite
-rwxr-xr-xbin/lugovaya_pump_mqtt_bot.py207
-rwxr-xr-xbin/mqtt_node_util.py51
-rw-r--r--include/py/homekit/config/config.py3
-rw-r--r--include/py/homekit/database/_base.py2
-rw-r--r--include/py/homekit/database/sqlite.py2
-rw-r--r--include/py/homekit/mqtt/_config.py2
-rw-r--r--include/py/homekit/mqtt/_wrapper.py21
-rw-r--r--include/py/homekit/mqtt/module/relay.py3
-rw-r--r--include/py/homekit/pio/products.py3
-rw-r--r--include/py/homekit/telegram/bot.py2
-rw-r--r--include/py/homekit/telegram/config.py11
11 files changed, 286 insertions, 21 deletions
diff --git a/bin/lugovaya_pump_mqtt_bot.py b/bin/lugovaya_pump_mqtt_bot.py
new file mode 100755
index 0000000..85402d1
--- /dev/null
+++ b/bin/lugovaya_pump_mqtt_bot.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python3
+import datetime
+import __py_include
+
+from enum import Enum
+from typing import Optional
+from telegram import ReplyKeyboardMarkup, User
+
+from homekit.config import config, AppConfigUnit
+from homekit.telegram import bot
+from homekit.telegram.config import TelegramBotConfig
+from homekit.telegram._botutil import user_any_name
+from homekit.mqtt import MqttNode, MqttPayload, MqttNodesConfig, MqttWrapper
+from homekit.mqtt.module.relay import MqttRelayState, MqttRelayModule
+from homekit.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload
+
+
+class LugovayaPumpMqttBotConfig(TelegramBotConfig, AppConfigUnit):
+ NAME = 'lugovaya_pump_mqtt_bot'
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ **TelegramBotConfig.schema(),
+ 'relay_node_id': {
+ 'type': 'string',
+ 'required': True
+ },
+ }
+
+ @staticmethod
+ def custom_validator(data):
+ relay_node_names = MqttNodesConfig().get_nodes(filters=('relay',), only_names=True)
+ if data['relay_node_id'] not in relay_node_names:
+ raise ValueError('unknown relay node "%s"' % (data['relay_node_id'],))
+
+
+config.load_app(LugovayaPumpMqttBotConfig)
+
+bot.initialize()
+bot.lang.ru(
+ start_message="Выберите команду на клавиатуре",
+ start_message_no_access="Доступ запрещён. Вы можете отправить заявку на получение доступа.",
+ unknown_command="Неизвестная команда",
+ send_access_request="Отправить заявку",
+ management="Админка",
+
+ enable="Включить",
+ enabled="Включен ✅",
+
+ disable="Выключить",
+ disabled="Выключен ❌",
+
+ status="Статус",
+ status_updated=' (обновлено %s)',
+
+ done="Готово 👌",
+ user_action_notification='Пользователь <a href="tg://user?id=%d">%s</a> <b>%s</b> насос.',
+ user_action_on="включил",
+ user_action_off="выключил",
+ date_yday="вчера",
+ date_yyday="позавчера",
+ date_at="в"
+)
+bot.lang.en(
+ start_message="Select command on the keyboard",
+ start_message_no_access="You have no access.",
+ unknown_command="Unknown command",
+ send_access_request="Send request",
+ management="Admin options",
+
+ enable="Turn ON",
+ enable_silently="Turn ON silently",
+ enabled="Turned ON ✅",
+
+ disable="Turn OFF",
+ disable_silently="Turn OFF silently",
+ disabled="Turned OFF ❌",
+
+ status="Status",
+ status_updated=' (updated %s)',
+
+ done="Done 👌",
+ user_action_notification='User <a href="tg://user?id=%d">%s</a> turned the pump <b>%s</b>.',
+ user_action_on="ON",
+ user_action_off="OFF",
+
+ date_yday="yesterday",
+ date_yyday="the day before yesterday",
+ date_at="at"
+)
+
+
+mqtt: MqttWrapper
+relay_state = MqttRelayState()
+relay_module: MqttRelayModule
+
+
+class UserAction(Enum):
+ ON = 'on'
+ OFF = 'off'
+
+
+# def on_mqtt_message(home_id, message: MqttPayload):
+# if isinstance(message, InitialDiagnosticsPayload) or isinstance(message, DiagnosticsPayload):
+# kwargs = dict(rssi=message.rssi, enabled=message.flags.state)
+# if isinstance(message, InitialDiagnosticsPayload):
+# kwargs['fw_version'] = message.fw_version
+# relay_state.update(**kwargs)
+
+
+async def notify(user: User, action: UserAction) -> None:
+ def text_getter(lang: str):
+ action_name = bot.lang.get(f'user_action_{action.value}', lang)
+ user_name = user_any_name(user)
+ return 'ℹ ' + bot.lang.get('user_action_notification', lang,
+ user.id, user_name, action_name)
+
+ await bot.notify_all(text_getter, exclude=(user.id,))
+
+
+@bot.handler(message='enable')
+async def enable_handler(ctx: bot.Context) -> None:
+ relay_module.switchpower(True)
+ await ctx.reply(ctx.lang('done'))
+ await notify(ctx.user, UserAction.ON)
+
+
+@bot.handler(message='disable')
+async def disable_handler(ctx: bot.Context) -> None:
+ relay_module.switchpower(False)
+ await ctx.reply(ctx.lang('done'))
+ await notify(ctx.user, UserAction.OFF)
+
+
+@bot.handler(message='status')
+async def status(ctx: bot.Context) -> None:
+ label = ctx.lang('enabled') if relay_state.enabled else ctx.lang('disabled')
+ if relay_state.ever_updated:
+ date_label = ''
+ today = datetime.date.today()
+ if today != relay_state.update_time.date():
+ yday = today - datetime.timedelta(days=1)
+ yyday = today - datetime.timedelta(days=2)
+ if yday == relay_state.update_time.date():
+ date_label = ctx.lang('date_yday')
+ elif yyday == relay_state.update_time.date():
+ date_label = ctx.lang('date_yyday')
+ else:
+ date_label = relay_state.update_time.strftime('%d.%m.%Y')
+ date_label += ' '
+ date_label += ctx.lang('date_at') + ' '
+ date_label += relay_state.update_time.strftime('%H:%M')
+ label += ctx.lang('status_updated', date_label)
+ await ctx.reply(label)
+
+
+async def start(ctx: bot.Context) -> None:
+ if ctx.user_id in config['bot']['users']:
+ await ctx.reply(ctx.lang('start_message'))
+ else:
+ buttons = [
+ [ctx.lang('send_access_request')]
+ ]
+ await ctx.reply(ctx.lang('start_message_no_access'),
+ markup=ReplyKeyboardMarkup(buttons, one_time_keyboard=False))
+
+
+@bot.exceptionhandler
+def exception_handler(e: Exception, ctx: bot.Context) -> bool:
+ return False
+
+
+@bot.defaultreplymarkup
+def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]:
+ buttons = [
+ [
+ ctx.lang('enable'),
+ ctx.lang('disable')
+ ],
+ # [ctx.lang('status')]
+ ]
+ # if ctx.user_id in config['bot']['admin_users']:
+ # buttons.append([ctx.lang('management')])
+ return ReplyKeyboardMarkup(buttons, one_time_keyboard=False)
+
+
+node_data = MqttNodesConfig().get_node(config.app_config['relay_node_id'])
+
+mqtt = MqttWrapper(client_id='lugovaya_pump_mqtt_bot')
+mqtt_node = MqttNode(node_id=config.app_config['relay_node_id'],
+ node_secret=node_data['password'])
+module_kwargs = {}
+try:
+ if node_data['relay']['legacy_topics']:
+ module_kwargs['legacy_topics'] = True
+except KeyError:
+ pass
+relay_module = mqtt_node.load_module('relay', **module_kwargs)
+# mqtt_node.add_payload_callback(on_mqtt_message)
+mqtt.add_node(mqtt_node)
+
+mqtt.connect_and_loop(loop_forever=False)
+
+bot.run(start_handler=start)
+
+mqtt.disconnect()
diff --git a/bin/mqtt_node_util.py b/bin/mqtt_node_util.py
index cf451fd..c1d457c 100755
--- a/bin/mqtt_node_util.py
+++ b/bin/mqtt_node_util.py
@@ -7,12 +7,37 @@ from typing import Optional
from argparse import ArgumentParser, ArgumentError
from homekit.config import config
-from homekit.mqtt import MqttNode, MqttWrapper, get_mqtt_modules
-from homekit.mqtt import MqttNodesConfig
+from homekit.mqtt import MqttNode, MqttWrapper, get_mqtt_modules, MqttNodesConfig
+from homekit.mqtt.module.relay import MqttRelayModule
+from homekit.mqtt.module.ota import MqttOtaModule
mqtt_node: Optional[MqttNode] = None
mqtt: Optional[MqttWrapper] = None
+relay_module: Optional[MqttOtaModule] = None
+relay_val = None
+
+ota_module: Optional[MqttRelayModule] = None
+ota_val = False
+
+no_wait = False
+stop_loop = False
+
+
+def on_mqtt_connect():
+ global stop_loop
+
+ if relay_module:
+ relay_module.switchpower(relay_val == 1)
+
+ if ota_val:
+ if not os.path.exists(arg.push_ota):
+ raise OSError(f'--push-ota: file \"{arg.push_ota}\" does not exists')
+ ota_module.push_ota(arg.push_ota, 1)
+
+ if no_wait:
+ stop_loop = True
+
if __name__ == '__main__':
nodes_config = MqttNodesConfig()
@@ -26,15 +51,21 @@ if __name__ == '__main__':
parser.add_argument('--legacy-relay', action='store_true')
parser.add_argument('--push-ota', type=str, metavar='OTA_FILENAME',
help='push OTA, receives path to firmware.bin')
+ parser.add_argument('--no-wait', action='store_true',
+ help='execute command and exit')
config.load_app(parser=parser, no_config=True)
arg = parser.parse_args()
+ if arg.no_wait:
+ no_wait = True
+
if arg.switch_relay is not None and 'relay' not in arg.modules:
raise ArgumentError(None, '--relay is only allowed when \'relay\' module included in --modules')
mqtt = MqttWrapper(randomize_client_id=True,
client_id='mqtt_node_util')
+ mqtt.add_connect_callback(on_mqtt_connect)
mqtt_node = MqttNode(node_id=arg.node_id,
node_secret=nodes_config.get_node(arg.node_id)['password'])
@@ -42,6 +73,8 @@ if __name__ == '__main__':
# must-have modules
ota_module = mqtt_node.load_module('ota')
+ ota_val = arg.push_ota
+
mqtt_node.load_module('diagnostics')
if arg.modules:
@@ -51,18 +84,16 @@ if __name__ == '__main__':
kwargs['legacy_topics'] = True
module_instance = mqtt_node.load_module(m, **kwargs)
if m == 'relay' and arg.switch_relay is not None:
- module_instance.switchpower(arg.switch_relay == 1)
+ relay_module = module_instance
+ relay_val = arg.switch_relay
try:
mqtt.connect_and_loop(loop_forever=False)
-
- if arg.push_ota:
- if not os.path.exists(arg.push_ota):
- raise OSError(f'--push-ota: file \"{arg.push_ota}\" does not exists')
- ota_module.push_ota(arg.push_ota, 1)
-
- while True:
+ while not stop_loop:
sleep(0.1)
except KeyboardInterrupt:
+ pass
+
+ finally:
mqtt.disconnect()
diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py
index 7d30a77..d424888 100644
--- a/include/py/homekit/config/config.py
+++ b/include/py/homekit/config/config.py
@@ -10,6 +10,7 @@ from argparse import ArgumentParser
from enum import Enum, auto
from os.path import join, isdir, isfile
from ..util import Addr
+from pprint import pprint
class MyValidator(cerberus.Validator):
@@ -140,7 +141,7 @@ class ConfigUnit(BaseConfigUnit):
schema['logging'] = {
'type': 'dict',
'schema': {
- 'logging': {'type': 'boolean'}
+ 'verbose': {'type': 'boolean'}
}
}
diff --git a/include/py/homekit/database/_base.py b/include/py/homekit/database/_base.py
index c01e62b..dcec9da 100644
--- a/include/py/homekit/database/_base.py
+++ b/include/py/homekit/database/_base.py
@@ -1,7 +1,7 @@
import os
-def get_data_root_directory(name: str) -> str:
+def get_data_root_directory() -> str:
return os.path.join(
os.environ['HOME'],
'.config',
diff --git a/include/py/homekit/database/sqlite.py b/include/py/homekit/database/sqlite.py
index 8b0c44c..1651a93 100644
--- a/include/py/homekit/database/sqlite.py
+++ b/include/py/homekit/database/sqlite.py
@@ -18,7 +18,7 @@ class SQLiteBase:
def __init__(self, name=None, path=None, check_same_thread=False):
if not path:
if not name:
- name = config.app_config['database_name']
+ name = config.app_name
database_path = _get_database_path(name)
else:
database_path = path
diff --git a/include/py/homekit/mqtt/_config.py b/include/py/homekit/mqtt/_config.py
index 9ba9443..e5f2c56 100644
--- a/include/py/homekit/mqtt/_config.py
+++ b/include/py/homekit/mqtt/_config.py
@@ -105,7 +105,7 @@ class MqttNodesConfig(ConfigUnit):
'relay': {
'type': 'dict',
'schema': {
- 'device_type': {'type': 'string', 'allowed': ['lamp', 'pump', 'solenoid'], 'required': True},
+ 'device_type': {'type': 'string', 'allowed': ['lamp', 'pump', 'solenoid', 'cooler'], 'required': True},
'legacy_topics': {'type': 'boolean'}
}
},
diff --git a/include/py/homekit/mqtt/_wrapper.py b/include/py/homekit/mqtt/_wrapper.py
index 3c2774c..5fc33fe 100644
--- a/include/py/homekit/mqtt/_wrapper.py
+++ b/include/py/homekit/mqtt/_wrapper.py
@@ -7,6 +7,8 @@ from ..util import strgen
class MqttWrapper(Mqtt):
_nodes: list[MqttNode]
+ _connect_callbacks: list[callable]
+ _disconnect_callbacks: list[callable]
def __init__(self,
client_id: str,
@@ -18,17 +20,30 @@ class MqttWrapper(Mqtt):
super().__init__(clean_session=clean_session,
client_id=client_id)
self._nodes = []
+ self._connect_callbacks = []
+ self._disconnect_callbacks = []
self._topic_prefix = topic_prefix
def on_connect(self, client: mqtt.Client, userdata, flags, rc):
super().on_connect(client, userdata, flags, rc)
for node in self._nodes:
node.on_connect(self)
+ for f in self._connect_callbacks:
+ try:
+ f()
+ except Exception as e:
+ self._logger.exception(e)
def on_disconnect(self, client: mqtt.Client, userdata, rc):
super().on_disconnect(client, userdata, rc)
for node in self._nodes:
node.on_disconnect()
+ for f in self._disconnect_callbacks:
+ try:
+ f()
+ except Exception as e:
+ self._logger.exception(e)
+
def on_message(self, client: mqtt.Client, userdata, msg):
try:
@@ -40,6 +55,12 @@ class MqttWrapper(Mqtt):
except Exception as e:
self._logger.exception(str(e))
+ def add_connect_callback(self, f: callable):
+ self._connect_callbacks.append(f)
+
+ def add_disconnect_callback(self, f: callable):
+ self._disconnect_callbacks.append(f)
+
def add_node(self, node: MqttNode):
self._nodes.append(node)
if self._connected:
diff --git a/include/py/homekit/mqtt/module/relay.py b/include/py/homekit/mqtt/module/relay.py
index e968031..5cbe09b 100644
--- a/include/py/homekit/mqtt/module/relay.py
+++ b/include/py/homekit/mqtt/module/relay.py
@@ -69,8 +69,7 @@ class MqttRelayModule(MqttModule):
mqtt.subscribe_module(self._get_switch_topic(), self)
mqtt.subscribe_module('relay/status', self)
- def switchpower(self,
- enable: bool):
+ def switchpower(self, enable: bool):
payload = MqttPowerSwitchPayload(secret=self._mqtt_node_ref.secret,
state=enable)
self._mqtt_node_ref.publish(self._get_switch_topic(),
diff --git a/include/py/homekit/pio/products.py b/include/py/homekit/pio/products.py
index a0e7a1f..5b40aae 100644
--- a/include/py/homekit/pio/products.py
+++ b/include/py/homekit/pio/products.py
@@ -3,6 +3,7 @@ import logging
from io import StringIO
from collections import OrderedDict
+from ..mqtt import MqttNodesConfig
_logger = logging.getLogger(__name__)
@@ -37,6 +38,8 @@ def platformio_ini(product_config: dict,
debug=False,
debug_network=False) -> str:
node_id = build_specific_defines['CONFIG_NODE_ID']
+ if node_id not in MqttNodesConfig().get_nodes().keys():
+ raise ValueError(f'node id "{node_id}" is not specified in the config!')
# defines
defines = {
diff --git a/include/py/homekit/telegram/bot.py b/include/py/homekit/telegram/bot.py
index 2efd9e4..f5f620a 100644
--- a/include/py/homekit/telegram/bot.py
+++ b/include/py/homekit/telegram/bot.py
@@ -266,7 +266,7 @@ class conversation:
return self.invoke(state, ctx)
return _invoke
- def invoke(self, state, ctx: Context):
+ async def invoke(self, state, ctx: Context):
self._logger.debug(f'invoke, state={state}')
for item in dir(self):
f = getattr(self, item)
diff --git a/include/py/homekit/telegram/config.py b/include/py/homekit/telegram/config.py
index 4c7d74b..5f41008 100644
--- a/include/py/homekit/telegram/config.py
+++ b/include/py/homekit/telegram/config.py
@@ -51,15 +51,15 @@ class TelegramBotConfig(ConfigUnit, ABC):
'type': 'dict',
'schema': {
'token': {'type': 'string', 'required': True},
- TelegramUserListType.USERS: {**TelegramBotConfig._userlist_schema(), 'required': True},
- TelegramUserListType.NOTIFY: TelegramBotConfig._userlist_schema(),
+ TelegramUserListType.USERS.value: {**TelegramBotConfig._userlist_schema(), 'required': True},
+ TelegramUserListType.NOTIFY.value: TelegramBotConfig._userlist_schema(),
}
}
}
@staticmethod
def _userlist_schema() -> dict:
- return {'type': 'list', 'schema': {'type': ['string', 'int']}}
+ return {'type': 'list', 'schema': {'type': ['string', 'integer']}}
@staticmethod
def custom_validator(data):
@@ -72,4 +72,7 @@ class TelegramBotConfig(ConfigUnit, ABC):
def get_user_ids(self,
ult: TelegramUserListType = TelegramUserListType.USERS) -> list[int]:
- return list(map(_user_id_mapper, self['bot'][ult.value])) \ No newline at end of file
+ try:
+ return list(map(_user_id_mapper, self['bot'][ult.value]))
+ except KeyError:
+ return [] \ No newline at end of file