Merge branch 'master' into website-python-rewrite
This commit is contained in:
commit
bae3534f5a
207
bin/lugovaya_pump_mqtt_bot.py
Executable file
207
bin/lugovaya_pump_mqtt_bot.py
Executable file
@ -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()
|
@ -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()
|
||||
|
@ -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'}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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'}
|
||||
}
|
||||
},
|
||||
|
@ -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:
|
||||
|
@ -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(),
|
||||
|
@ -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 = {
|
||||
|
@ -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)
|
||||
|
@ -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]:
|
||||
try:
|
||||
return list(map(_user_id_mapper, self['bot'][ult.value]))
|
||||
except KeyError:
|
||||
return []
|
Loading…
x
Reference in New Issue
Block a user