summaryrefslogtreecommitdiff
path: root/bin/pump_bot.py
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2024-02-17 03:08:25 +0300
committerEvgeny Zinoviev <me@ch1p.io>2024-02-17 03:08:25 +0300
commit0ce2e41a2bad790c5232fafb4b6ed631ca8cd957 (patch)
treefd401495b87cae8c95a4c4edf2c851c8177b6069 /bin/pump_bot.py
parente9fc2c1835f7ac8e072919df81a6661c6308dea9 (diff)
parentb7f1d55c9b4de4d21b11e5615a5dc8be0d4e883c (diff)
merge with master
Diffstat (limited to 'bin/pump_bot.py')
-rwxr-xr-xbin/pump_bot.py297
1 files changed, 297 insertions, 0 deletions
diff --git a/bin/pump_bot.py b/bin/pump_bot.py
new file mode 100755
index 0000000..e00e844
--- /dev/null
+++ b/bin/pump_bot.py
@@ -0,0 +1,297 @@
+#!/usr/bin/env python3
+import __py_include
+import sys
+import asyncio
+
+from enum import Enum
+from typing import Optional, Union
+from telegram import ReplyKeyboardMarkup, User
+from time import time
+from datetime import datetime
+
+from homekit.config import config, is_development_mode, AppConfigUnit
+from homekit.telegram import bot
+from homekit.telegram.config import TelegramBotConfig, TelegramUserListType
+from homekit.telegram._botutil import user_any_name
+from homekit.relay.sunxi_h3_client import RelayClient
+from homekit.mqtt import MqttNode, MqttWrapper, MqttPayload, MqttNodesConfig, MqttModule
+from homekit.mqtt.module.relay import MqttPowerStatusPayload, MqttRelayModule
+from homekit.mqtt.module.temphum import MqttTemphumDataPayload
+from homekit.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload
+
+
+if __name__ != '__main__':
+ print(f'this script can not be imported as module', file=sys.stderr)
+ sys.exit(1)
+
+
+mqtt_nodes_config = MqttNodesConfig()
+
+
+class PumpBotUserListType(TelegramUserListType):
+ SILENT = 'silent_users'
+
+
+class PumpBotConfig(AppConfigUnit, TelegramBotConfig):
+ NAME = 'pump_bot'
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ **super(TelegramBotConfig).schema(),
+ PumpBotUserListType.SILENT: TelegramBotConfig._userlist_schema(),
+ 'watering_relay_node': {'type': 'string'},
+ 'pump_relay_addr': cls._addr_schema()
+ }
+
+ @staticmethod
+ def custom_validator(data):
+ relay_node_names = mqtt_nodes_config.get_nodes(filters=('relay',), only_names=True)
+ if data['watering_relay_node'] not in relay_node_names:
+ raise ValueError(f'unknown relay node "{data["watering_relay_node"]}"')
+
+
+config.load_app(PumpBotConfig)
+
+mqtt: MqttWrapper
+mqtt_node: MqttNode
+mqtt_relay_module: Union[MqttRelayModule, MqttModule]
+
+time_format = '%d.%m.%Y, %H:%M:%S'
+
+watering_mcu_status = {
+ 'last_time': 0,
+ 'last_boot_time': 0,
+ 'relay_opened': False,
+ 'ambient_temp': 0.0,
+ 'ambient_rh': 0.0,
+}
+
+bot.initialize()
+bot.lang.ru(
+ start_message="Выберите команду на клавиатуре",
+ unknown_command="Неизвестная команда",
+
+ enable="Включить",
+ enable_silently="Включить тихо",
+ enabled="Насос включен ✅",
+
+ disable="Выключить",
+ disable_silently="Выключить тихо",
+ disabled="Насос выключен ❌",
+
+ start_watering="Включить полив",
+ stop_watering="Отключить полив",
+
+ status="Статус насоса",
+ watering_status="Статус полива",
+
+ done="Готово 👌",
+ sent="Команда отправлена",
+
+ user_action_notification='Пользователь <a href="tg://user?id=%d">%s</a> <b>%s</b> насос.',
+ user_watering_notification='Пользователь <a href="tg://user?id=%d">%s</a> <b>%s</b> полив.',
+ user_action_on="включил",
+ user_action_off="выключил",
+ user_action_watering_on="включил",
+ user_action_watering_off="выключил",
+)
+bot.lang.en(
+ start_message="Select command on the keyboard",
+ unknown_command="Unknown command",
+
+ enable="Turn ON",
+ enable_silently="Turn ON silently",
+ enabled="The pump is turned ON ✅",
+
+ disable="Turn OFF",
+ disable_silently="Turn OFF silently",
+ disabled="The pump is turned OFF ❌",
+
+ start_watering="Start watering",
+ stop_watering="Stop watering",
+
+ status="Pump status",
+ watering_status="Watering status",
+
+ done="Done 👌",
+ sent="Request sent",
+
+ user_action_notification='User <a href="tg://user?id=%d">%s</a> turned the pump <b>%s</b>.',
+ user_watering_notification='User <a href="tg://user?id=%d">%s</a> <b>%s</b> the watering.',
+ user_action_on="ON",
+ user_action_off="OFF",
+ user_action_watering_on="started",
+ user_action_watering_off="stopped",
+)
+
+
+class UserAction(Enum):
+ ON = 'on'
+ OFF = 'off'
+ WATERING_ON = 'watering_on'
+ WATERING_OFF = 'watering_off'
+
+
+def get_relay() -> RelayClient:
+ relay = RelayClient(host=config.app_config['pump_relay_addr'].host,
+ port=config.app_config['pump_relay_addr'].port)
+ relay.connect()
+ return relay
+
+
+async def on(ctx: bot.Context, silent=False) -> None:
+ get_relay().on()
+ futures = [ctx.reply(ctx.lang('done'))]
+ if not silent:
+ futures.append(notify(ctx.user, UserAction.ON))
+ await asyncio.gather(*futures)
+
+
+async def off(ctx: bot.Context, silent=False) -> None:
+ get_relay().off()
+ futures = [ctx.reply(ctx.lang('done'))]
+ if not silent:
+ futures.append(notify(ctx.user, UserAction.OFF))
+ await asyncio.gather(*futures)
+
+
+async def watering_on(ctx: bot.Context) -> None:
+ mqtt_relay_module.switchpower(True)
+ await asyncio.gather(
+ ctx.reply(ctx.lang('sent')),
+ notify(ctx.user, UserAction.WATERING_ON)
+ )
+
+
+async def watering_off(ctx: bot.Context) -> None:
+ mqtt_relay_module.switchpower(False)
+ await asyncio.gather(
+ ctx.reply(ctx.lang('sent')),
+ notify(ctx.user, UserAction.WATERING_OFF)
+ )
+
+
+async def notify(user: User, action: UserAction) -> None:
+ notification_key = 'user_watering_notification' if action in (UserAction.WATERING_ON, UserAction.WATERING_OFF) else 'user_action_notification'
+
+ 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(notification_key, 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:
+ await on(ctx)
+
+
+@bot.handler(message='enable_silently')
+async def enable_s_handler(ctx: bot.Context) -> None:
+ await on(ctx, True)
+
+
+@bot.handler(message='disable')
+async def disable_handler(ctx: bot.Context) -> None:
+ await off(ctx)
+
+
+@bot.handler(message='start_watering')
+async def start_watering(ctx: bot.Context) -> None:
+ await watering_on(ctx)
+
+
+@bot.handler(message='stop_watering')
+async def stop_watering(ctx: bot.Context) -> None:
+ await watering_off(ctx)
+
+
+@bot.handler(message='disable_silently')
+async def disable_s_handler(ctx: bot.Context) -> None:
+ await off(ctx, True)
+
+
+@bot.handler(message='status')
+async def status(ctx: bot.Context) -> None:
+ await ctx.reply(
+ ctx.lang('enabled') if get_relay().status() == 'on' else ctx.lang('disabled')
+ )
+
+
+def _get_timestamp_as_string(timestamp: int) -> str:
+ if timestamp != 0:
+ return datetime.fromtimestamp(timestamp).strftime(time_format)
+ else:
+ return 'unknown'
+
+
+@bot.handler(message='watering_status')
+async def watering_status(ctx: bot.Context) -> None:
+ buf = ''
+ if 0 < watering_mcu_status["last_time"] < time()-1800:
+ buf += '<b>WARNING! long time no reports from mcu! maybe something\'s wrong</b>\n'
+ buf += f'last report time: <b>{_get_timestamp_as_string(watering_mcu_status["last_time"])}</b>\n'
+ if watering_mcu_status["last_boot_time"] != 0:
+ buf += f'boot time: <b>{_get_timestamp_as_string(watering_mcu_status["last_boot_time"])}</b>\n'
+ buf += 'relay opened: <b>' + ('yes' if watering_mcu_status['relay_opened'] else 'no') + '</b>\n'
+ buf += f'ambient temp & humidity: <b>{watering_mcu_status["ambient_temp"]} °C, {watering_mcu_status["ambient_rh"]}%</b>'
+ await ctx.reply(buf)
+
+
+@bot.defaultreplymarkup
+def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]:
+ buttons = []
+ if ctx.user_id in config.app_config.get_user_ids(PumpBotUserListType.SILENT):
+ buttons.append([ctx.lang('enable_silently'), ctx.lang('disable_silently')])
+ buttons.append([ctx.lang('enable'), ctx.lang('disable'), ctx.lang('status')],)
+ buttons.append([ctx.lang('start_watering'), ctx.lang('stop_watering'), ctx.lang('watering_status')])
+
+ return ReplyKeyboardMarkup(buttons, one_time_keyboard=False)
+
+
+def mqtt_payload_callback(mqtt_node: MqttNode, payload: MqttPayload):
+ global watering_mcu_status
+
+ types_the_node_can_send = (
+ InitialDiagnosticsPayload,
+ DiagnosticsPayload,
+ MqttTemphumDataPayload,
+ MqttPowerStatusPayload
+ )
+ for cl in types_the_node_can_send:
+ if isinstance(payload, cl):
+ watering_mcu_status['last_time'] = int(time())
+ break
+
+ if isinstance(payload, InitialDiagnosticsPayload):
+ watering_mcu_status['last_boot_time'] = int(time())
+
+ elif isinstance(payload, MqttTemphumDataPayload):
+ watering_mcu_status['ambient_temp'] = payload.temp
+ watering_mcu_status['ambient_rh'] = payload.rh
+
+ elif isinstance(payload, MqttPowerStatusPayload):
+ watering_mcu_status['relay_opened'] = payload.opened
+
+
+mqtt = MqttWrapper(client_id='pump_bot')
+mqtt_node = MqttNode(node_id=config.app_config['watering_relay_node'])
+if is_development_mode():
+ mqtt_node.load_module('diagnostics')
+
+mqtt_node.load_module('temphum')
+mqtt_relay_module = mqtt_node.load_module('relay')
+
+mqtt_node.add_payload_callback(mqtt_payload_callback)
+
+mqtt.connect_and_loop(loop_forever=False)
+
+bot.run()
+
+try:
+ mqtt.disconnect()
+except:
+ pass