Home Assistant и Keenetic: Управление по REST

14 ноября, 2023

Несмотря на то, что у HA есть штатная интеграция для сетевых устройств Keenetic, она предоставляет только возможность отслеживать присутствие устройств в сети (presence detection). Но, как обычно это бывает, со временем появляется необходимость в получении дополнительных данных или управлении интернет-центром.

Маршрутизаторы Keenetic предоставляют несколько вариантов управления – это классический веб-интерфейс, доступ к консоли по telnet или ssh, а так же RESTful API. Несмотря на такой богатый выбор, интегрировать эти устройства без дополнительных телодвижений не получится. Использование telnet зачастую невозможно, так как этот пакет попросту отсутствует в докер контейнере HA, ssh сервер Кинетиков не поддерживает авторизацию пользователей по ключам (только логин\пароль), а REST интерфейс использует собственный механизм авторизации, с которым компоненты HA работать не умеют.

Так как использовать telnet и ssh в самом популярном варианте инсталляции HA без дополнительных прослоек в виде контейнеров, серверов, и т.д. проблематично, стоит сконцентрировать внимание на REST API.
Описание механизма аутентификации можно найти здесь. Так же в свое время техническая поддержка Кинетиков, присылала мне пакет для Node.js реализующий авторизацию на интернет-центрах.

JS-код текстом
const axios = require('axios');
const md5 = require('js-md5');
const jsSHA = require('jssha');
const KEENETIC_IP = '192.168.1.1';
const LOGIN = 'admin';
const PASSWORD = '1';
const AUTH_URL = `http://${KEENETIC_IP}/auth`;
const RCI_URL_PREFIX = `http://${KEENETIC_IP}/rci`;
const getSha256Hash = (text) => {
  const shaObj = new jsSHA('SHA-256', 'TEXT');
  shaObj.update(text);
  return shaObj.getHash('HEX');
};
const getMd5HashArg = (login, realm, password) => `${login}:${realm}:${password}`;
const getCryptedPass = (login, password, token, realm) => {
  const md5Arg = getMd5HashArg(login, realm, password);
  const md5Hash = md5(md5Arg);
  return getSha256Hash(token + md5Hash);
}
const authPromise = new Promise((resolve, reject) => {
  axios.get(AUTH_URL)
    .then((res) => {
      console.log('NO PASSWORD');
      resolve();
    })
    .catch((err) => {
      const headers = err.response.headers;
      const token = headers['x-ndm-challenge'];
      const realm = headers['x-ndm-realm'];
      // DEBUG OUTPUT
      console.log(`TOKEN=<${token}>`);
      console.log(`REALM=<${realm}>`);
      console.log(headers);
      const cookies = headers['set-cookie'][0].split(';');
      // DEBUG OUTPUT
      console.log(`COOKIES=<${cookies}`);
      axios({
          method: 'post',
          url: AUTH_URL,
          headers: {
              Cookie: cookies[0]
          },
          data: {
            login: LOGIN,
            password: getCryptedPass(LOGIN, PASSWORD, token, realm)
          }
        })
        .then((res) => {
          console.log('AUTHORIZED!');
          resolve(cookies[0]);
        })
        .catch((err) => {
          console.log('ERROR', err);
          reject();
        });
    });
});
authPromise.then((cookie) => {
  let headers = {};
  if (cookie) {
    headers.Cookie = cookie;
  }
  axios({
    method: 'get',
    url: `${RCI_URL_PREFIX}/show/ip/hotspot`,
    headers: headers
  })
  .then(requestObj => {
    console.log(requestObj.data);
  });
})

К сожалению, этот вариант не подходит для стандартной интеграции REST, так как она поддерживает только basic или digest аутентификацию.
Вопрос можно было бы решить написанием кастомного компонента, но, это не ко мне =).

Тем не менее, все же существует возможность общения с Кинетиками по REST без авторизации. Это достаточно “грязный” хак, крайне небезопасный по умолчанию и поэтому требующий в обязательном порядке дополнительных мер – настройки фаервола и\или использования VLAN, благо все это Кинетик умеет.

Дело в том, что “внутри” Keenetic OS во многом используется тот же REST API, только без авторизации. Доступен он по адресу http://127.0.0.1:79 и все что нам остается это создать правило трансляции портов, направляющее трафик из локальной сети на “внутренний” порт 79/tcp.
Сделать это можно создав следующее правило переадресации портов:

Или через telnet\ssh:

ip static tcp 192.168.0.0 255.255.255.0 81 127.0.0.1 79 !RCI for HA

Где 192.168.0.0 и 255.255.255.0 подсеть, откуда будут обращения к REST’у, а 81 – порт, на который вы будете обращаться.

После создания такого правила, по адресу http://192.168.0.1:81/rci будет доступен REST API позволяющий управлять практически всеми функциями интернет-центра без авторизации. И повторюсь еще раз – это опасно!
Позаботьтесь как минимум о настройке фаервола, ограничив доступ к 81 порту всем, кроме избранных адресов.

Теперь можно приступить к изучению API.
Некоторую информацию можно почерпнуть из англоязычной документации на интернет-центры, например здесь (Appendix B, HTTP API). Как ни странно, но в русской версии документации этой информации нет. Зато в любой версии документации есть достаточно подробное описание консольных команд, которые пригодятся для формирования нужных запросов к API.
Также полезным может быть использование Developer Tools браузеров, настраивая интернет-центр через веб-интерфейс можно увидеть все выполненные запросы к API:

Для отладки запросов\ответов удобно использовать веб-интерфейс по адресу http://192.168.0.1/a (вкладка REST):

Далее я приведу несколько примеров использования REST платформы HA для управления Кинетиками.
(вместо 192.168.0.1 нужно указать IP-адрес вашего интернет-центра)

Сенсор IP адреса выданного провайдером (имя интерфейса по умолчанию ISP):

sensor:

  - platform: rest
    name: Keenetic MGTS IP
    unique_id: 2aa13ade-9d53-48a6-8183-f238b13804a4
    resource: http://192.168.0.1:81/rci/show/interface?name=ISP
    scan_interval: 300
    value_template: "{{ value_json.address }}"
    json_attributes:
      - mask
      - mac

Сенсоры загрузки CPU и RAM:

sensor:

  - platform: rest
    name: Keenetic CPU Usage
    unique_id: ded628f3-2510-4894-953a-247ad62de663
    unit_of_measurement: '%'
    resource: http://192.168.0.1:81/rci/show/system
    value_template: "{{ value_json.cpuload }}"
      
  - platform: rest
    name: Keenetic RAM Usage
    unique_id: 3a225571-d10f-4405-8e0a-b3015a4273b4
    unit_of_measurement: '%'
    resource: http://192.168.0.1:81/rci/show/system
    value_template: >
          {%- set mem = value_json.memory-%}
          {%- set memfree = mem.split('/')[0]|int(0)-%}
          {%- set memtotal = mem.split('/')[1]|int(1)-%}
          {{ (memfree*100/memtotal)| round(2) }}

Скорость интерфейса (GigabitEthernet1):

sensor:

  - platform: rest
    name: Keenetic MGTS Speed TX
    unique_id: f454be65-7854-4e28-86cb-3300dd200468
    resource: http://192.168.0.1:81/rci/show/interface/stat?name=GigabitEthernet1
    scan_interval: 30
    value_template: "{{ (value_json.txspeed / 1250000) | round(2) }}"
    unit_of_measurement: 'Mbit/s'
    json_attributes:
      - txbytes
      - txerrors
      - txspeed
  - platform: rest
    resource: http://192.168.0.1:81/rci/show/interface/stat?name=GigabitEthernet1
    name: Keenetic MGTS Speed RX
    unique_id: 0e5c3581-1e57-42c7-b60f-38141d76165e
    scan_interval: 30
    value_template: "{{ (value_json.rxspeed / 1250000) | round(2) }}"
    unit_of_measurement: 'Mbit/s'
    json_attributes:
      - rxbytes
      - rxerrors
      - rxspeed

Статус VPN подключения (интерфейс Wireguard0):

sensor:

  - platform: rest
    name: Keenetic Wireguard0
    unique_id: 84fcece4-cc5b-43b9-a57a-3ef39c367d9b
    resource: http://192.168.0.1:81/rci/show/interface/Wireguard0
    value_template: "{{ value_json.state }}"
    json_attributes:
      - address
      - link
      - uptime

Switch для управления гостевой WiFi сетью (таким образом можно включать\выключать любой интерфейс):

switch:

  - platform: rest
    name: keenetic_guest_wifi
    unique_id: fe415708-fafa-4008-9656-96599ba1ba13
    resource: "http://192.168.0.1:81/rci/interface/GuestWiFi"
    body_on:  '{"up":"true"}'
    body_off: '{"down":"true"}'
    is_on_template: "{{ value_json.up is defined }}"

Включение или отключение интернета для конкретного устройства (mac-адрес нужно заменить на ваш):

switch:

  - platform: rest
    name: keenetic_inet_xbox_01
    unique_id: 2c5447b0-bcda-478f-bd72-167f56fd0821
    resource: "http://192.168.0.1:81/rci/ip/hotspot/host"
    body_on:  '{"mac":"a4:50:46:d4:9b:d0", "access":"permit"}'
    body_off: '{"mac":"a4:50:46:d4:9b:d0", "access":"deny" }'
    is_on_template: "{{ 'permit' in value_json | selectattr('mac', 'match', 'a4:50:46:d4:9b:d0') | join }}"

Включение или отключение VPN для устройства:
(Policy4 – предварительно созданная политика доступа в интернет с подключением через VPN, mac заменить на свой)

switch:

  - platform: rest
    name: keenetic_vpn_mi8
    unique_id: 888c7caa-4352-446a-8b4b-415a4676fa5a
    resource: "http://192.168.0.1:81/rci/ip/hotspot/host"
    body_on: '{"mac":"a4:50:46:d4:9b:d0", "policy":"Policy4"}'
    body_off: '{"mac":"a4:50:46:d4:9b:d0", "policy": false }'
    is_on_template: "{{ 'Policy4' in value_json | selectattr('mac', 'match', 'a4:50:46:d4:9b:d0') | join }}"

В целом, используя REST API можно управлять практически всеми функциями интернет-центров, а с Home Assistant возможности становятся практически безграничными.

– Алиса, включи VPN для телефона!