diff options
-rw-r--r-- | doc/polaris_pwk_1725cgld.md | 34 | ||||
-rw-r--r-- | src/polaris/protocol.py | 6 | ||||
-rw-r--r-- | src/polaris_kettle_bot.py | 149 |
3 files changed, 133 insertions, 56 deletions
diff --git a/doc/polaris_pwk_1725cgld.md b/doc/polaris_pwk_1725cgld.md index edd439b..ad13cef 100644 --- a/doc/polaris_pwk_1725cgld.md +++ b/doc/polaris_pwk_1725cgld.md @@ -1,3 +1,29 @@ +## Bot configuration + +``` +[bot] +token = "bot token" +users = [ id1, id2 ] +#notify_users = [ 1, 2 ] + +[mqtt] +host = "192.168.88.49" +port = 1883 +client_id = "kettle_bot" + +[logging] +verbose = true +default_fmt = true + +[kettle] +mac = 'kettle mac' +token = 'kettle token' +temp_max = 100 +temp_min = 30 +temp_step = 5 +``` + + ## Random research notes ### Device features @@ -42,9 +68,9 @@ From `devices.json`: }, ``` -### Random notes +### Protocol commands -All commands, from `com/polaris/iot/api/commands`: +From `com/polaris/iot/api/commands`: ``` $ grep -A1 -r "public byte getType()" . ./CmdAccessControl.java: public byte getType() { @@ -207,4 +233,6 @@ $ grep -A1 -r "public byte getType()" . -- ./CmdWifiList.java: public byte getType() { ./CmdWifiList.java- return -127; -```
\ No newline at end of file +``` + +See also class `com/syncleiot/iottransport/commands/CmdHardware`.
\ No newline at end of file diff --git a/src/polaris/protocol.py b/src/polaris/protocol.py index 5d7390f..f26d25e 100644 --- a/src/polaris/protocol.py +++ b/src/polaris/protocol.py @@ -477,9 +477,9 @@ class HandshakeResponseMessage(CmdIncomingMessage): # Apparently, some hardware info. -# On the other hand, if you look at com.syncleiot.iottransport.commands.CmdHardware, its mqtt topic is "mcu_firmware". -# My device returns 1.1.1. The thing uses on ESP8266 MCU under the hood (or, more precisely, under a piece of cheap -# plastic), so maybe 1.1.1 is the MCU fw revision. +# On the other hand, if you look at com.syncleiot.iottransport.commands.CmdHardware, its mqtt topic says "mcu_firmware". +# My device returns 1.1.1. The kettle uses on ESP8266 ESP-12F MCU under the hood (or, more precisely, under a piece of +# cheap plastic), so maybe 1.1.1 is some MCU ROM version. class DeviceHardwareMessage(CmdIncomingMessage): TYPE = 143 # -113 diff --git a/src/polaris_kettle_bot.py b/src/polaris_kettle_bot.py index bad1b3a..2fb988e 100644 --- a/src/polaris_kettle_bot.py +++ b/src/polaris_kettle_bot.py @@ -12,6 +12,7 @@ from home.bot import Wrapper, Context, text_filter, handlermethod from home.api.types import BotType from home.mqtt import MQTTBase from home.config import config +from home.util import chunks from polaris import ( Kettle, PowerType, @@ -21,7 +22,7 @@ from polaris import ( ConnectionStatus ) import polaris.protocol as kettle_proto -from typing import Optional, Tuple, List +from typing import Optional, Tuple, List, Union from collections import namedtuple from functools import partial from datetime import datetime @@ -42,10 +43,45 @@ from telegram.ext import ( logger = logging.getLogger(__name__) kc: Optional[KettleController] = None bot: Optional[Wrapper] = None -RenderedContent = Tuple[str, Optional[InlineKeyboardMarkup]] +RenderedContent = Tuple[str, Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]]] tasks_lock = threading.Lock() +def run_tasks(tasks: queue.SimpleQueue, done: callable): + def next_task(r: Optional[kettle_proto.MessageResponse]): + if r is not None: + try: + assert r is not False, 'server error' + except AssertionError as exc: + logger.exception(exc) + tasks_lock.release() + return done(False) + + if not tasks.empty(): + task = tasks.get() + args = task[1:] + args.append(next_task) + f = getattr(kc.kettle, task[0]) + f(*args) + else: + tasks_lock.release() + return done(True) + + tasks_lock.acquire() + next_task(None) + + +def temperature_emoji(temp: int) -> str: + if temp > 90: + return '🔥' + elif temp >= 40: + return '♨️' + elif temp >= 35: + return '🌡' + else: + return '❄️' + + class KettleInfoListener: @abstractmethod def info_updated(self, field: str): @@ -331,6 +367,14 @@ class Renderer: return html, None @classmethod + def temp(cls, ctx: Context, choices) -> RenderedContent: + buttons = [] + for chunk in chunks(choices, 5): + buttons.append([f'{temperature_emoji(n)} {n}' for n in chunk]) + buttons.append([ctx.lang('back')]) + return ctx.lang('select_temperature'), ReplyKeyboardMarkup(buttons) + + @classmethod def turned_on(cls, ctx: Context, target_temp: int, current_temp: int, @@ -342,11 +386,15 @@ class Renderer: html = ctx.lang('enabling') else: if not reached: - emoji = '♨️' if current_temp <= 90 else '🔥' - html = ctx.lang('enabled', emoji, target_temp) + html = ctx.lang('enabled') + + # target temperature + html += '\n' + html += ctx.lang('enabled_target', temperature_emoji(target_temp), target_temp) # current temperature html += '\n' + html += temperature_emoji(current_temp) + ' ' html += ctx.lang('status_current_temp', current_temp) else: html = ctx.lang('enabled_reached', current_temp) @@ -403,30 +451,6 @@ class Renderer: ]) -def run_tasks(tasks: queue.SimpleQueue, done: callable): - def next_task(r: Optional[kettle_proto.MessageResponse]): - if r is not None: - try: - assert r is not False, 'server error' - except AssertionError as exc: - logger.exception(exc) - tasks_lock.release() - return done(False) - - if not tasks.empty(): - task = tasks.get() - args = task[1:] - args.append(next_task) - f = getattr(kc.kettle, task[0]) - f(*args) - else: - tasks_lock.release() - return done(True) - - tasks_lock.acquire() - next_task(None) - - MUTUpdate = namedtuple('MUTUpdate', 'message_id, user_id, finished, changed, delete, html, markup') @@ -532,25 +556,26 @@ class KettleBot(Wrapper): start_message="Выберите команду на клавиатуре", unknown_command="Неизвестная команда", unexpected_callback_data="Ошибка: неверные данные", - enable_70="♨️ 70 °C", - enable_80="♨️ 80 °C", - enable_90="♨️ 90 °C", - enable_100="🔥 100 °C", disable="❌ Выключить", server_error="Ошибка сервера", + back="🔙 Назад", # /status status_not_connected="😟 Связь с чайником не установлена", - status_on="✅ Чайник <b>включён</b> (до <b>%d °C</b>)", - status_off="❌ Чайник <b>выключен</b>", + status_on="🟢 Чайник <b>включён</b> (до <b>%d °C</b>)", + status_off="🔴 Чайник <b>выключен</b>", status_current_temp="Сейчас: <b>%d °C</b>", status_update_time="<i>Обновлено %s</i>", status_update_time_fmt="%d %b в %H:%M:%S", - # enable + # /temp + select_temperature="Выберите температуру:", + + # enable/disable enabling="💤 Чайник включается...", disabling="💤 Чайник выключается...", - enabled="%s Чайник <b>включён</b>.\nЦель: <b>%d °C</b>", + enabled="🟢 Чайник <b>включён</b>.", + enabled_target="%s Цель: <b>%d °C</b>", enabled_reached="✅ <b>Готово!</b> Чайник вскипел, температура <b>%d °C</b>.", disabled="✅ Чайник <b>выключен</b>.", please_wait="⏳ Ожидайте..." @@ -560,42 +585,56 @@ class KettleBot(Wrapper): start_message="Select command on the keyboard", unknown_command="Unknown command", unexpected_callback_data="Unexpected callback data", - enable_70="♨️ 70 °C", - enable_80="♨️ 80 °C", - enable_90="♨️ 90 °C", - enable_100="🔥 100 °C", disable="❌ Turn OFF", server_error="Server error", + back="🔙 Back", # /status - not_connected="😟 Connection has not been established", - status_on="✅ Turned <b>ON</b>! Target: <b>%d °C</b>", - status_off="❌ Turned <b>OFF</b>", + not_connected="😟 No connection", + status_on="🟢 Turned <b>ON</b>! Target: <b>%d °C</b>", + status_off="🔴 Turned <b>OFF</b>", status_current_temp="Now: <b>%d °C</b>", status_update_time="<i>Updated on %s</i>", status_update_time_fmt="%b %d, %Y at %H:%M:%S", - # enable + # /temp + select_temperature="Select a temperature:", + + # enable/disable enabling="💤 Turning on...", disabling="💤 Turning off...", - enabled="%s The kettle is <b>turned ON</b>.\nTarget: <b>%d °C</b>", - enabled_reached="✅ It's <b>done</b>! The kettle has boiled, the temperature is <b>%d °C</b>.", + enabled="🟢 The kettle is <b>turned ON</b>.", + enabled_target="%s Target: <b>%d °C</b>", + enabled_reached="✅ <b>Done</b>! The kettle has boiled, the temperature is <b>%d °C</b>.", disabled="✅ The kettle is <b>turned OFF</b>.", please_wait="⏳ Please wait..." ) + self.primary_choices = (70, 80, 90, 100) + self.all_choices = range( + config['kettle']['temp_min'], + config['kettle']['temp_max']+1, + config['kettle']['temp_step']) + # commands self.add_handler(CommandHandler('status', self.status)) + self.add_handler(CommandHandler('temp', self.temp)) - # messages - for temp in (70, 80, 90, 100): - self.add_handler(MessageHandler(text_filter(self.lang.all(f'enable_{temp}')), self.wrap(partial(self.on, temp)))) + # enable messages + for temp in self.primary_choices: + self.add_handler(MessageHandler(text_filter(f'{temperature_emoji(temp)} {temp}'), self.wrap(partial(self.on, temp)))) + for temp in self.all_choices: + self.add_handler(MessageHandler(text_filter(f'{temperature_emoji(temp)} {temp}'), self.wrap(partial(self.on, temp)))) + # disable message self.add_handler(MessageHandler(text_filter(self.lang.all('disable')), self.off)) + # back message + self.add_handler(MessageHandler(text_filter(self.lang.all('back')), self.back)) + def markup(self, ctx: Optional[Context]) -> Optional[ReplyKeyboardMarkup]: buttons = [ - [ctx.lang(f'enable_{x}') for x in (70, 80, 90, 100)], + [f'{temperature_emoji(n)} {n}' for n in self.primary_choices], [ctx.lang('disable')] ] return ReplyKeyboardMarkup(buttons, one_time_keyboard=False) @@ -669,6 +708,16 @@ class KettleBot(Wrapper): update_time=kc.info.update_time) return ctx.reply(text, markup=markup) + @handlermethod + def temp(self, ctx: Context): + text, markup = Renderer.temp( + ctx, choices=self.all_choices) + return ctx.reply(text, markup=markup) + + @handlermethod + def back(self, ctx: Context): + self.start(ctx) + if __name__ == '__main__': config.load('polaris_kettle_bot') |