Интеграция с Google Home. 2 - хранение состояний
Я продолжаю рассказ о том, как сделать интеграцию с Google Home своими руками. В этом проекте я буду интегрировать несколько домашних кондиционеров, которые исходно управляются с пульта по infrared. Интеграция с Google Home означает возможность сказать “Hey G, turn on air conditioner” и получить желаемый результат.
В этой части я расскажу, как осуществляется хранение состояния (стейтов) управляемых устройств в Firebase Realtime Database, и как управлять устройствами на основе этих данных.
Полный список заметок этой серии:
- Интеграция с Google Home. 1 - устройства и команды
- Интеграция с Google Home. 2 - хранение состояний (вы здесь)
- Интеграция с Google Home. 3 - создание проекта в Google Assistant
- Интеграция с Google Home. 4 - Обзор Google Home API
- … продолжение следует
- IR интерфейс, Raspberry и LIRC
- Smart Home Device: получение обновлений из Firebase Realtime Database
Firebase Realtime Database как persistant storage и как средство связи
Мне нужен persistent storage для хранения состояния устройств. Я использую Firebase Realtime Database. В базе данных я буду хранить текущее (оно же желаемое) состояние для каждого устройства, и менять его по камандам от Google Home.
Также я использую Firebase Realtime Database как интерфейс между “облачной” частью моего дома и конечными исполнителями команд. Исполнители, которые должны отправлять команды устройствам (помним, что наши устройства - это управляемые по IR кодниционеры, телевизоры и другие домашние гаджеты?), будут подписываться на обновление этой базы данных, и при изменении отправлять устройству соответствующую команду.
О том, как подписываться на обновлениями, я писал в предыдущей статье этого цикла, здесь мы этот код расширим.
Это не очень хорошая архитектура, когда конечное устройство обращается к базе данных напрямую, но для нашего прототипа сойдет.
Access to DB
Поскольку Firebase Realtime Database находится в том же Firebase проекте, что и Functions, никакие дополнительные ключи или токены нам не нужны.
import * as admin from 'firebase-admin'
admin.initializeApp();
const firebaseRef = admin.database().ref('/');
TypeScript для меня новый язык, иногда он удивляет и радует. В данном случае, сменив импорт с
JavaScript-style const admin = require('firebase-admin');
на TypeScript-style, который выше,
я получил много подсказок о неправильной обработке Promise.
Структура
Поскольку единственное, что сейчас нужно хранить, это список стейтов, и стейты одного девайса могут храниться вместе, я так и сделаю. В лабе предлагается хранить стейты каждого trait отдельно, но я в этом никакой пользы не вижу.
Дальше код расскажет все сам.
import {ApiClientObjectMap} from "actions-on-google/src/common";
export async function getDeviceState(deviceId: string): Promise<ApiClientObjectMap<any>> {
return firebaseRef
.child(deviceId)
.once('value')
.then((state) => {
return state.val();
});
}
export async function saveDeviceState(deviceId: string,
newState: ApiClientObjectMap<any>) {
await firebaseRef
.child(deviceId)
.update({
...newState,
...{lastChange: new Date().getTime()}
});
}
export async function updateDeviceState(deviceId: string,
update: ApiClientObjectMap<any>) {
await saveDeviceState(deviceId,
{
...await getDeviceState(deviceId),
...update
});
}
Подписка на изменения
Обратите внимание на поле lastChange
, которое изменяется в функции saveDeviceState()
.
Наш сервер будет записывать новое состояние стейтов каждый раз при получении команды из Google Home.
И одновременно с этим всегда будет обновляться поле lastChange
.
Это нужно для того, чтобы исполнитель команд мог увидеть необходимость что-то сделать, даже если желаемый статус
устройства не изменился. Например, по какой-то внешней причине кондиционер оказался включенным, хотя по состоянию
в базе данных он выключен. Пользователь может несколько раз подряд просить Google Assistant выключить кондиционер,
и наш сервер несколько раз подряд получит одну и ту же команду. Но, поскольку в базе данных уже указано,
что кондиционер выключен, никакого заметного обновления данных не произойдет. И если исполнитель будет ориентироваться
только на желаемое состояние устройства в базе, он не увидит необходимость повторно выключить кондиционер.
Чтобы избежать это, исполнитель будет ориентироваться на смену lastChange
,
и при изменении пошлет команду, соотсутствующую желаемому состоянию еще раз.
Как исполнитель может подписаться на измененения в Firebase Realtime Database, я рассказывал в предыдущей статье. Теперь, когда формат данных известен, эту историю можно продолжить:
def action(old, new):
if new['lastChange'] > old['lastChange']:
if not new['on']:
acTurnOff()
else:
acTemperature(new['thermostatTemperatureSetpoint'])
Еще раз обратите внимание, что константы-ключи on
и thermostatTemperatureSetpoint
- это имена
стейтов соответствующих trait-ов.
Отправка команд по infrared тоже была мной описана в одной из предыдущих статей, теперь можно добавить и ее:
def acTurnOff():
print('turn off')
subprocess.call(["irsend", "send_once", "DOWN_AC_RAW", "OFF"])
time.sleep(2)
subprocess.call(["irsend", "send_once", "DOWN_AC_RAW", "OFF"])
def acTemperature(temp):
print('temperature: {}'.format(temp))
if temp < 18:
temp = 18
if temp > 30:
temp = 30
subprocess.call(["irsend", "send_once", "DOWN_AC_RAW", "SET_{}".format(temp)])
time.sleep(2)
subprocess.call(["irsend", "send_once", "DOWN_AC_RAW", "SET_{}".format(temp)])
Здесь видна хитрость: исполнитель отправляет каждую команду дважды. Это компенсирует некоторую ненадежность такого способа управления, а писк устройства при получении команды становится более заметным.
Продолжение: Часть 3 - создание проекта в Google Assistant