Веб-интерфейс для кофеварки на Ajenti через HTCPCP

от автора

Надоело бегать за кофе и жать кнопки на кофеварке? Сделай к ней веб-интерфейс!



Весь код из статьи доступен на Github.

Подготовка

Скачаем Ajenti c PyPI и распакуем.
Устанавливаем зависимости:

sudo pip install -Ur requirements.txt 

Кроме того, понадобится поддерживающая HTCPCP кофеварка, а если ее у вас нет — можно воспользоваться простым эмулятором, который поддерживает добавление молока и шоколада!

HTCPCP-клиент

Создадим в ajenti/plugins папку htcpcp, а в ней — структуру файлов для плагина:

ajenti/plugins/htcpcp ├── __init__.py ├── client.py └── layout     └── config.xml  

В __init__.py помещается информация о плагине:

from ajenti.api import * from ajenti.plugins import *   info = PluginInfo(     title='HTCPCP Example',     icon='coffee',     dependencies=[         PluginDependency('main')     ], )   def init():     import client     import main 

В client.py создадим класс для работы с протоколом HTCPCP:

import requests  from ajenti.api import * from ajenti.plugins.configurator.api import ClassConfigEditor   @plugin class HTCPCPClientConfigEditor (ClassConfigEditor):     """     A plugin that handles editing of HTCPCPClient's classconfig     """     title = 'HTCPCP Client'     icon = 'coffee'      def init(self):         self.append(self.ui.inflate('htcpcp:config'))  # htcpcp/layout/config.xml   class CoffeeAddition (object):     def __init__(self, name):         self.name = name         self.selected = False   @plugin class HTCPCPClient (BasePlugin):     classconfig_editor = HTCPCPClientConfigEditor  # connect this plugin with a ConfigEditor     default_classconfig = {'url': 'htcpcp://127.0.0.1:5000'}      def init(self):         self.additions = []      def check_connectivity(self):         resp = requests.request('PROPFIND', self.get_url())         if resp.status_code == 418:             raise Exception('This coffee pot is a teapot')      def refresh(self):         resp = requests.request('PROPFIND', self.get_url())         self.additions = [CoffeeAddition(x) for x in resp.headers['Additions-List'].split(';')]      def get_url(self):         return self.classconfig['url'].replace('htcpcp', 'http')      def brew(self):         """         Brew coffee with selected additions         """         return requests.request('BREW', self.get_url(), headers={             'Accept-Additions': ';'.join(x.name for x in self.additions if x.selected)         })      def retrieve(self):         return requests.request('GET', self.get_url())  

Обратите внимание, что здесь мы используем ConfigEditor API, чтобы дать пользователю возможность перенастроить класс HTCPCPClient на использование другого URL кофеварки.

Создадим пустой раздел в панели (main.py):

from ajenti.api import * from ajenti.plugins.main.api import SectionPlugin  from client import HTCPCPClient   @plugin class CoffeePlugin (SectionPlugin):     """     A HTCPCP capable coffeepot control plugin     """      def init(self):         self.title = 'Coffeepot'         self.icon = 'coffee'         self.category = _('System')          # IoC: получаем объект HTCPCPClient         self.pot = HTCPCPClient.get()  

Добавим немного UI для настройки класса и запустим панель:

<bind:dict id="bind"> <!-- Плагин настроек свяжет этот элемент с атрибутом .classconfig класса -->     <formline text="Coffeepot URL">         <textbox bind="url" />     </formline> </bind:dict> 
make run 

Теперь, перейдя в раздел Configure > Plugins, мы видим наш плагин и можем настроить URL.

Интерфейс

Давайте создадим несколько красивых кнопочек!

htcpcp/layout/main.xml:

    <hc>         <button id="brew" icon="arrow-right" text="Brew" />         <button id="retrieve" icon="coffee" text="Retrieve" />         <button id="refresh" icon="refresh" text="Refresh" />     </hc> 

htcpcp/main.py:

from ajenti.api import * from ajenti.plugins.main.api import SectionPlugin from ajenti.ui import on from ajenti.ui.binder import Binder  from client import HTCPCPClient   @plugin class CoffeePlugin (SectionPlugin):     """     A HTCPCP capable coffeepot control plugin     """      def init(self):         self.title = 'Coffeepot'         self.icon = 'coffee'         self.category = _('System')         self.append(self.ui.inflate('htcpcp:main'))  # htcpcp/layout/main.xml         self.pot = HTCPCPClient.get()      def on_page_load(self):         try:             self.pot.check_connectivity()         except Exception, e:             self.context.notify('error', 'Could not access the coffee pot: %s!' % str(e))             self.context.launch('configure-plugin', plugin=self.pot) # попросим плагин Configure показать окно настроек      @on('brew', 'click')     def on_brew(self):         resp = self.pot.brew()         if resp.status_code == 200:             self.context.notify('info', 'Brewing')         else:             self.context.notify('error', resp.text)      @on('refresh', 'click')     def on_refresh(self):         # обновим список кофейных добавок         self.pot.refresh()      @on('retrieve', 'click')     def on_retrieve(self):         resp = self.pot.retrieve()         if resp.status_code == 200:             self.context.notify('info', resp.text)         else:             self.context.notify('error', resp.text) 

Теперь можно жать кнопочки и варить кофе 🙂

Отображаем данные

Осталось только сделать отображение поддерживаемых кофейных добавок и их выбор. Для этого удобнее всего использовать связывание данных с UI напрямую, используя класс Binder.

Добавим в main.xml элементы для отображения списка чекбоксов для добавок:

<vc>     <body>         <pad id="pot-root"> <!-- id для быстрого нахождения -->             <bind:collection bind="additions"> <!-- связываем список объектов CoffeeAdditions из HTCPCPClient.additions -->                 <vc bind="__items"> <!-- элементы будут помещаться в этот контейнер <vc> -->                     <label style="bold" text="Available additions:" />                 </vc>                 <bind:template> <!-- шаблон элемента -->                     <checkbox bind:value="selected" bind:text="name" /> <!-- связываем value чекбокса с addition.selected, а text - c addition.name -->                 </bind:template>             </bind:collection>         </pad>     </body>      <hc>         <button id="brew" icon="arrow-right" text="Brew" />         <button id="retrieve" icon="coffee" text="Retrieve" />         <button id="refresh" icon="refresh" text="Refresh" />     </hc> </vc> 

А в main.py используем Binder, чтобы наполнить интерфейс данными, а затем обновить состояние добавок (выбрана или нет, исходя из состояния чекбоксов):

from ajenti.api import * from ajenti.plugins.main.api import SectionPlugin from ajenti.ui import on from ajenti.ui.binder import Binder  from client import HTCPCPClient   @plugin class CoffeePlugin (SectionPlugin):     """     A HTCPCP capable coffeepot control plugin     """      def init(self):         self.title = 'Coffeepot'         self.icon = 'coffee'         self.category = _('System')          self.append(self.ui.inflate('htcpcp:main'))          self.pot = HTCPCPClient.get()          # настраиваем binder для нашего интерфейса (связываем HTCPCPClient self.pot и элемент с id=pot-root)         self.binder = Binder(self.pot, self.find('pot-root'))      def on_page_load(self):         try:             self.pot.check_connectivity()         except Exception, e:             self.context.notify('error', 'Could not access the coffee pot: %s!' % str(e))             self.context.launch('configure-plugin', plugin=self.pot)             return                   if not self.pot.additions:             # Если добавки еще не получены, получаем их             self.pot.refresh()             # Помещаем данные в UI             self.binder.populate()      @on('brew', 'click')     def on_brew(self):         # Обновляем данные из UI         self.binder.update()         resp = self.pot.brew()         if resp.status_code == 200:             self.context.notify('info', 'Brewing')         else:             self.context.notify('error', resp.text)      @on('refresh', 'click')     def on_refresh(self):         self.pot.refresh()         # Помещаем данные в UI         self.binder.populate()      @on('retrieve', 'click')     def on_retrieve(self):         resp = self.pot.retrieve()         if resp.status_code == 200:             self.context.notify('info', resp.text)         else:             self.context.notify('error', resp.text)  

Готово 🙂

Напоминаю, что код из статьи доступен на Github, а также не забывайте читать документацию и подглядывать в примеры.

ссылка на оригинал статьи http://habrahabr.ru/post/215083/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *