Skip to content

Умная кофеварка

Alex X edited this page Sep 18, 2023 · 2 revisions

Интеграции

Дополнительное ПО

  • Caddy - для автоматического HTTPS
  • DuckDNS - бесплатное доменное имя

configuration.yaml

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 127.0.0.1

yandex_dialogs:
  file: dialogs.py

dialogs.py

from homeassistant.core import HomeAssistant

from custom_components.morph_numbers import MORPH


def make_response(event: dict, text: str, end_session=False) -> dict:
    return {
        "version": event["version"],
        "session": event["session"],
        "response": {"text": text, "end_session": end_session},
    }


def get_slots(intent: dict) -> dict:
    return {k: v["value"] for k, v in intent["slots"].items()}


def morph(value: str, text: str) -> str:
    return (
        MORPH.number_with_text(value, text, as_text=False)
        if value != "unavailable"
        else ""
    )


def state_response(event: dict, context: dict, end_session=False) -> dict:
    hass: HomeAssistant = context["hass"]

    product = hass.states.get("select.jura_product").state
    if product == "unknown":
        return intent_list(event)

    stren = hass.states.get("select.jura_coffee_strength").state
    temp = hass.states.get("select.jura_temperature").state
    coffee = hass.states.get("number.jura_water_amount").state
    milk = hass.states.get("number.jura_milk_foam_amount").state
    water = hass.states.get("number.jura_bypass").state
    pause = hass.states.get("number.jura_milk_break").state

    text = morph(coffee, "миллилитр")

    if temp == "High":
        text += " горячего"
    elif temp == "Low":
        text += " холодного"

    text += " кофе крепости " + stren

    if value := morph(milk, "секунда"):
        text += f" и {value} молока"

    if value := morph(water, "миллилитр"):
        text += f" и {value} воды"

    if pause != "unavailable":
        text += " с паузой " + pause

    return make_response(event, text, end_session)


def set_state(hass: HomeAssistant, entity_id: str, value: int | str):
    state = hass.states.get(entity_id)
    if state.state == "unavailable":
        return

    if entity_id.startswith("select"):
        if isinstance(value, int):
            # important! options from 1 to len
            options: list[str] = state.attributes["options"]
            if value > len(options):
                value = len(options)
            elif value < 1:
                value = 1
            value = options[value - 1]

        hass.services.call(
            "select",
            "select_option",
            service_data={"option": value},
            blocking=True,
            target={"entity_id": entity_id},
        )

    elif entity_id.startswith("number"):
        if value < state.attributes["min"]:
            value = state.attributes["min"]
        if value > state.attributes["max"]:
            value = state.attributes["max"]

        hass.services.call(
            "number",
            "set_value",
            service_data={"value": value},
            blocking=True,
            target={"entity_id": entity_id},
        )


PRODUCTS = {
    "coffee": "Coffee",
    "cappuccino": "Cappuccino",
    "espresso": "Espresso",
    "latte": "Latte Macchiato",
    "barista": "Cafe Barista",
    "lungo_barista": "Barista Lungo",
    "flat_white": "1 Flat White",
    "cortado": "Cortado",
    "macchiato": "Espresso Macchiato",
    "doppio": "Espresso Doppio",
}

TEMPERATURES = {"low": "Low", "normal": "Normal", "high": "High"}


def intent_product(event: dict, context: dict, slots: dict) -> dict:
    hass: HomeAssistant = context["hass"]

    if option := PRODUCTS.get(slots.get("product")):
        set_state(hass, "select.jura_product", option)

    if option := TEMPERATURES.get(slots.get("temperature")):
        set_state(hass, "select.jura_temperature", option)

    if value := slots.get("coffee"):
        set_state(hass, "number.jura_water_amount", value)

    if value := slots.get("milk"):
        set_state(hass, "number.jura_milk_foam_amount", value)

    if value := slots.get("water"):
        set_state(hass, "number.jura_bypass", value)

    if value := slots.get("strength"):
        set_state(hass, "select.jura_coffee_strength", value)

    # check for instant make
    command: str = event["request"]["command"]
    if command.startswith("make "):
        intent_make(event, context)
        return state_response(event, context, end_session=True)

    return state_response(event, context)


PARAM_ENTITIES = {
    "coffee": "number.jura_water_amount",
    "milk": "number.jura_milk_foam_amount",
    "water": "number.jura_bypass",
    "strength": "select.jura_coffee_strength",
    "temperature": "select.jura_temperature",
}


def intent_param(event: dict, context: dict, slots: dict) -> dict:
    param: str = slots["param"]
    if "-" in param:
        # temperature-inc, strength-dec
        param, direction = param.split("-")
    else:
        direction = slots.get("direction")  # inc, dec

    entity_id: str = PARAM_ENTITIES[param]

    hass: HomeAssistant = context["hass"]
    state = hass.states.get(entity_id)
    if state.state == "unavailable":
        return make_response(event, "Нельзя изменить для этого напитка")

    if entity_id.startswith("select"):
        # important! options from 1 to len
        options: list = state.attributes["options"]
        value_max = len(options)
        value_min = 1
        value_old = 1 + options.index(state.state)
    else:
        value_max = state.attributes["max"]
        value_min = state.attributes["min"]
        value_old = int(state.state)

    value = slots.get("value", 1)

    if direction == "inc":
        value = value_old + value
    elif direction == "dec":
        value = value_old - value

    if value > value_max:
        value = value_max
    elif value < value_min:
        value = value_min

    set_state(hass, entity_id, value)

    return state_response(event, context)


def intent_make(event: dict, context: dict) -> dict:
    # return make_response(event, "Блокировка")
    hass: HomeAssistant = context["hass"]
    hass.services.call(
        "button",
        "press",
        target={"entity_id": "button.jura_make"},
    )

    return make_response(event, "Поехали!", end_session=True)


def intent_list(event: dict):
    return make_response(
        event, "Обычный, эспрессо, капучино, латте, бариста, flat white"
    )


def default(event: dict, context: dict) -> dict:
    hass: HomeAssistant = context["hass"]
    hass.services.call(
        # "persistent_notification", "create",
        "telegram_bot",
        "send_message",
        service_data={"message": event["request"]["command"]},
    )

    return make_response(event, "я не понимаю")


def handler(event: dict, context: dict) -> dict:
    intents: dict = event["request"].get("nlu", {}).get("intents")

    if intent := intents.get("coffee.product"):
        return intent_product(event, context, get_slots(intent))

    if intent := intents.get("coffee.param"):
        return intent_param(event, context, get_slots(intent))

    if "coffee.make" in intents:
        return intent_make(event, context)

    if "coffee.list" in intents:
        return intent_list(event)

    if "coffee.settings" in intents:
        return state_response(event, context)

    if event["request"]["command"] == "":
        return make_response(event, "Какой кофе желаете?")

    return default(event, context)

dialogs.yandex.ru

Сущности

entity product:
    values:
        coffee:
            кофе
            обычный
            простой
        cappuccino:
            капучино
        espresso:
            эспрессо
        latte:
            латте
        barista:
            бариста
            баристу
            по листу
        lungo_barista:
            лунго бариста
            лонго бариста
            лунка бариста
            лунга бариста
            гунга бариста
        flat_white:
            flat white
            led white
            red white
            red light
            left right
            лэд вайт
        cortado:
            кортадо
            кортана
        macchiato:
            маккиато
            макиато
            мотиватор
            котята
            гладиатор
            эспрессо маккиато
        doppio:
            доппио
            дай пиво
            крапива
            да пио
            тапио
            эспрессо доппио

entity param:
    lemma: true
    values:
        coffee:
            кофе
        milk:
            молоко
        water:
            вода
        strength:
            крепость
        temperature:
            температура
        strength-inc:
            крепче
        strength-dec:
            слабее
        temperature-inc:
            горячее
            погорячее
        temperature-dec:
            холоднее

entity temperature:
    lemma: true
    values:
        low:
            холодный
            прохладный
        normal:
            телпый
            нормальный
        high:
            горячий

entity direction:
    lemma: true
    values:
        inc:
            увеличить
            повысить
            поднять
            добавить
            прибавить
            больше
            побольше
        dec:
            уменьшить
            опустить
            отнять
            снизить
            меньше
            поменьше

coffee.product

root:
    %lemma
    [(($temperature)? $product)? ($coffee)? ($milk молока)? ($water воды)? (крепости $strength)?]

slots:
    temperature:
        type: temperature
        source: $temperature
    product:
        type: product
        source: $product
    coffee:
        type: YANDEX.NUMBER
        source: $coffee
    milk:
        type: YANDEX.NUMBER
        source: $milk
    water:
        type: YANDEX.NUMBER
        source: $water
    strength:
        type: YANDEX.NUMBER
        source: $strength

$coffee: $YANDEX.NUMBER
$milk: $YANDEX.NUMBER
$water: $YANDEX.NUMBER
$strength: $YANDEX.NUMBER

filler:
    %lemma
    make
    сделать|приготовить|налить|установить|хочу|давай
    количество|единиц|кофе
    а|и|с|на
    миллилитров|секунд

coffee.param

root:
    [$direction? $param $value?]

slots:
    direction:
        type: direction
        source: $direction
    param:
        type: param
        source: $param
    value:
        type: YANDEX.NUMBER
        source: $value

$value: $YANDEX.NUMBER

filler:
    %lemma
    сделать|приготовить|налить|изобразить|установить|хочу|давай
    количество|миллилитров|секунд|единиц|кофе|а
    кофе|чуточку|немного|капельку
    на

coffee.make

root:
    %lemma
    приготовь|сделать|запустить|готовь|давай|поехали|пойдёт|хорошо|наливай|вари

coffee.list

root:
    %lemma
    есть|умеешь|список

filler:
    %lemma
    какие|что|огласи|расскажи|весь|про|напитки|ты|еще
    make

coffee.settings

root:
    %lemma
    повтори
    ещё раз
    что (там|выбрано)
    какие (настройки|параметры)

docker-compose.yml

version: '3'
services:
  caddy:
    image: caddy:latest
    network_mode: host
    restart: unless-stopped
    volumes:
      - ~/docker/caddy/Caddyfile:/etc/caddy/Caddyfile
      - ~/docker/caddy/data:/data
      - ~/docker/caddy/config:/config

Caddyfile

{
	email mymail@gmail.com
	ocsp_stapling off
}

myhome.duckdns.org {
	reverse_proxy 127.0.0.1:8123
}
Clone this wiki locally