polaris_kettle_bot: add /temp command and more
This commit is contained in:
parent
f2d94cd93e
commit
5b4cadc2b6
@ -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;
|
||||
```
|
||||
```
|
||||
|
||||
See also class `com/syncleiot/iottransport/commands/CmdHardware`.
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
@ -330,6 +366,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,
|
||||
@ -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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user