298 lines
9.2 KiB
Python
Executable File
298 lines
9.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
import include_homekit
|
||
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
|