Skip to content

Commit

Permalink
Merge pull request #11 from hugobloem/refactor-mqtt-topic-handling
Browse files Browse the repository at this point in the history
Refactor mqtt topic handling, add convenience script, and siteId separation.
  • Loading branch information
hugobloem authored Apr 11, 2023
2 parents ed58048 + eb712e3 commit 9f780a0
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 21 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,4 @@ managed_components

# Secrets file
secrets.h
sites.yaml
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ https://user-images.githubusercontent.com/42470993/226731674-cff14709-fd51-44b7-
To get started please copy secrets_template.h to secrets.h and edit the variables in there. After that you can flash your ESP-BOX using esptool. I recommend using the Visual Studio Code ESP-IDF plugin as it installs all the required programs for you and flashed the device seamlessly.

## Managing voice commands
As of now voice commands can be sending MQTT messages to the `esp-ha/config/add_cmd` topic. As data you should provide a json like this: `{"text": "<your voice command>", "phonetic": "<phonetic voice command"}`. The `text` entry is the command you would like to send to Home Assistant/Rhasspy for recognition. The `phonetic` entry is the phonetic version of it. This phonetic version can be generated using the following python command `python esp-ha\managed_components\espressif__esp-sr\tool\multinet_g2p.py -t <your voice command>`.
As of now voice commands can be sending MQTT messages to the `esp-ha-speech/config/add_cmd` topic. As data you should provide a json like this: `{"text": "<your voice command>", "phonetic": "<phonetic voice command", "siteId": "<your-siteId>"}`. The `text` entry is the command you would like to send to Home Assistant/Rhasspy for recognition. The `phonetic` entry is the phonetic version of it. This phonetic version can be generated using the following python command `python esp-ha\managed_components\espressif__esp-sr\tool\multinet_g2p.py -t <your voice command>`. `siteId` is used to seperate different esp32s, so you can for example have one in the living room and one in the kitchen and they will only listen to the messages meant for that device.

To delete all existing commands send an MQTT message to `esp-ha/config/rm_all` with payload `{"confirm": "yes"}`. Note that there are now no voice commands in the system, thus trying to invoke the wake word will result in a crash.
Another option to add commands is to use the convenience script [`configure_sites.py`](./configure_sites.py). To get started create a `sites.yaml` file from the [`sites_template.yaml`](./sites_template.yaml) file. Most options are straightforward but note that under the `sites` tag multiple sites (or satellites) can be configured, each with their own set of devices. The Python script will fetch intent templates from the [Home Assistant intents repo](https://github.com/home-assistant/intents), it will then create some sentences and phonemes for the given entities and send to each site. At the moment this only supports turning on and off entities under the 'lights' tag.

To delete all existing commands send an MQTT message to `esp-ha-speech/config/rm_all` with payload `{"confirm": "yes", "siteId": "<your-siteId>"}`. Note that there are now no voice commands in the system, thus trying to invoke the wake word will result in a crash.
114 changes: 114 additions & 0 deletions configure_sites.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'''
Load sites/sattelites from a file and configure the commands.
The site configuration is loaded from sites.yaml, the intents are loaded from the github.com/home-assistant/intents repo.
'''

import re
import time

import requests
import yaml
from g2p_en import G2p
from paho.mqtt import client as mqtt_client

base_repo = 'https://raw.githubusercontent.com/home-assistant/intents/main/sentences/en/'
intent_scripts = ['homeassistant_HassTurnOff.yaml', 'homeassistant_HassTurnOn.yaml']
common_script = '_common.yaml'

# Load sites
with open('sites.yaml', 'r') as f:
conf = yaml.safe_load(f)
sites = conf['sites']

# Load intents from repo
intents = {}
for script in intent_scripts:
r = requests.get(base_repo + script)
if r.status_code == 200:
ryaml = yaml.safe_load(r.text)
for intent, data in ryaml['intents'].items():
intents[intent] = data['data'][0]['sentences']
else:
SystemError('Failed to load intent script: ' + script)


# Load expansion rules
r = requests.get(base_repo + common_script)
if r.status_code == 200:
ryaml = yaml.safe_load(r.text)
expansions = ryaml['expansion_rules']
for k, v in expansions.items():
expansions[k] = v[1:-1].split('|') # Remove quotes and split


# Construct sentences
sentences = [
'turn on the <name>',
'turn off the <name>',
'turn on <name>',
'turn off <name>',
'switch on the <name>',
'switch off the <name>',
'switch on <name>',
'switch off <name>',
'activate the <name>',
'deactivate the <name>',
'activate <name>',
'deactivate <name>',
]

def english_g2p(text_list, alphabet=None):
g2p = G2p()
outs = []
out = ''
if alphabet is None:
alphabet={"AE1": "a", "N": "N", " ": " ", "OW1": "b", "V": "V", "AH0": "c", "L": "L", "F": "F", "EY1": "d", "S": "S", "B": "B", "R": "R", "AO1": "e", "D": "D", "AH1": "c", "EH1": "f", "OW0": "b", "IH0": "g", "G": "G", "HH": "h", "K": "K", "IH1": "g", "W": "W", "AY1": "i", "T": "T", "M": "M", "Z": "Z", "DH": "j", "ER0": "k", "P": "P", "NG": "l", "IY1": "m", "AA1": "n", "Y": "Y", "UW1": "o", "IY0": "m", "EH2": "f", "CH": "p", "AE0": "a", "JH": "q", "ZH": "r", "AA2": "n", "SH": "s", "AW1": "t", "OY1": "u", "AW2": "t", "IH2": "g", "AE2": "a", "EY2": "d", "ER1": "k", "TH": "v", "UH1": "w", "UW2": "o", "OW2": "b", "AY2": "i", "UW0": "o", "AH2": "c", "EH0": "f", "AW0": "t", "AO2": "e", "AO0": "e", "UH0": "w", "UH2": "w", "AA0": "n", "AY0": "i", "IY2": "m", "EY0": "d", "ER2": "k", "OY2": "u", "OY0": "u"}

for item in text_list:
item = item.split(",")
for phrase in item:
labels = g2p(phrase)
for char in labels:
if char not in alphabet:
print("skip %s, not found in alphabet")
continue
else:
out += alphabet[char]
if phrase != item[-1]:
out += ','
outs.append(out)
out = ''

return outs

site_sentences = {}
for siteId, entities in sites.items():
site_sentences[siteId] = {'text': []}
for entity in entities['lights']: # TODO: Add other entities
for sentence in sentences:
site_sentences[siteId]['text'].append(sentence.replace('<name>', entity))
site_sentences[siteId]['phonetic'] = english_g2p(site_sentences[siteId]['text'])

assert len(site_sentences[siteId]['text']) == len(site_sentences[siteId]['phonetic'])
assert len(site_sentences[siteId]['text']) <= 200

# Connect to MQTT
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected to MQTT")
else:
print("Failed to connect, return code %d\n", rc)

client = mqtt_client.Client()
client.username_pw_set(conf['mqtt']['username'], conf['mqtt']['password'])
client.on_connect = on_connect
client.connect(conf['mqtt']['host'], conf['mqtt']['port'], 60)

# Send intents
for siteId, data in site_sentences.items():
for i, (text, phonetic) in enumerate(zip(data['text'], data['phonetic'])):
message = f'{{"text": "{text}", "phonetic": "{phonetic}", "siteId": "{siteId}"}}'
client.publish(f'{conf["mqtt"]["topic"]}/add_cmd', message)
print(f'Sent {i+1}/{len(data["text"])}: {message}')
time.sleep(0.5)
46 changes: 33 additions & 13 deletions main/app/app_api_mqtt.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include <string.h>
#include "esp_system.h"
#include "esp_event.h"
#include "esp_check.h"
#include "esp_err.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
Expand All @@ -10,6 +12,8 @@
#include "esp_log.h"
#include "mqtt_client.h"

#include "cJSON.h"

#include "app_api_mqtt.h"
#include "app_hass.h"
#include "ui_net_config.h"
Expand All @@ -33,37 +37,53 @@ esp_err_t data_handler(char *topic_, char *data, int topic_len, int data_len)
char topic[512];
strncpy(topic, topic_, topic_len);
topic[topic_len] = '\0';
char *subtopic = strtok(topic, "/");

cJSON *jData = cJSON_ParseWithLength(data, data_len);
if (jData == NULL) {
ESP_LOGE(TAG, "Error parsing json");
cJSON_Delete(jData);
return ESP_FAIL;
}

cJSON *siteId = cJSON_GetObjectItemCaseSensitive(jData, "siteId");
if (cJSON_IsString(siteId) == 0) {
ESP_LOGI(TAG, "No siteId.");
cJSON_Delete(jData);
return ESP_OK;
} else if (strcmp(siteId->valuestring, MQTT_SITE_ID) != 0) {
ESP_LOGI(TAG, "siteId does not match.");
cJSON_Delete(jData);
return ESP_OK;
}

// TODO: change to strtok
// topic starts with "hermes/"
if (strncmp(topic, "hermes/", 7) == 0) {
// Handle hermes messages
ESP_LOGI(TAG, "hermes message: %s", jData);
// app_hass_handle_hermes(topic, jData);
ESP_LOGI(TAG, "subtopic: %s", subtopic);

// HERMES MESSAGES"
if (strcmp(subtopic, "hermes") == 0) {
// Handle hermes messages
ESP_LOGI(TAG, "hermes message: %s", cJSON_Print(jData));


} else if (strncmp(topic, "esp-ha-speech/", 7) == 0) {
// ESP-HA MESSAGES"
} else if (strcmp(subtopic, "esp-ha-speech") == 0) {
// Handle esp-ha messages
ESP_LOGI(TAG, "esp-ha message: %s", jData);
ESP_LOGI(TAG, "esp-ha-speech message: %s", cJSON_Print(jData));

if (strncmp(topic, "esp-ha/config/add_cmd", 21) == 0) {
subtopic = strtok(NULL, "/");
if (strcmp(subtopic, "add_cmd") == 0) {
// Handle config messages
ESP_LOGI(TAG, "adding command");
app_hass_add_cmd_from_msg(jData);

} else if (strncmp(topic, "esp-ha/config/rm_all", 24) == 0) {
} else if (strcmp(subtopic, "rm_all") == 0) {
// Handle config messages
ESP_LOGI(TAG, "removing all commands");
app_hass_rm_all_cmd(jData);

}
} else {
ESP_LOGW(TAG, "Unknown subtopic: %s", subtopic);
}
cJSON_Delete(jData);
return ESP_OK;
}

Expand Down Expand Up @@ -149,7 +169,7 @@ void app_api_mqtt_start(void)

// Subscribe to hermes and configuration topics
esp_mqtt_client_subscribe(client, "hermes/#", 0);
esp_mqtt_client_subscribe(client, "esp-ha/#", 0);
esp_mqtt_client_subscribe(client, "esp-ha-speech/#", 0);

}

Expand Down
4 changes: 0 additions & 4 deletions main/app/app_hass.c
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,13 @@ void app_hass_add_cmd_from_msg(cJSON *root) {
cJSON *sr_phn = cJSON_GetObjectItemCaseSensitive(root, "phonetic");
if (sr_txt == NULL || sr_phn == NULL) {
ESP_LOGE(TAG, "Error parsing text");
cJSON_Delete(root);
return;
} else {
// Add sr command to speech recognition
app_hass_add_cmd(sr_txt->valuestring, sr_phn->valuestring, true);

app_hass_write_cmd_to_nvs(keynum, sr_txt->valuestring, sr_phn->valuestring);
ESP_LOGI(TAG, "Added command: %s; %s", sr_txt->valuestring, sr_phn->valuestring);
cJSON_Delete(root);
return;
}
}
Expand All @@ -210,7 +208,6 @@ void app_hass_rm_all_cmd(cJSON *root) {
cJSON *sr_txt = cJSON_GetObjectItemCaseSensitive(root, "confirm");
if (sr_txt == NULL) {
ESP_LOGE(TAG, "Error parsing text");
cJSON_Delete(root);
return;
} else if (strcmp(sr_txt->valuestring, "yes") == 0) {
// remove sr command from speech recognition
Expand All @@ -223,7 +220,6 @@ void app_hass_rm_all_cmd(cJSON *root) {
app_hass_rm_cmds_from_nvs();

ESP_LOGI(TAG, "Removed all commands");
cJSON_Delete(root);
return;
}
}
Expand Down
5 changes: 3 additions & 2 deletions main/app/app_hass.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once
#include <esp_err.h>
#include "cJSON.h"

#ifdef __cplusplus
extern "C" {
Expand All @@ -11,8 +12,8 @@ bool app_hass_is_connected(void);
void app_hass_send_cmd(char *cmd);

void app_hass_add_cmd(char *cmd, char *phoneme, bool commit);
void app_hass_add_cmd_from_msg(char *msg);
void app_hass_rm_all_cmd();
void app_hass_add_cmd_from_msg(cJSON *root);
void app_hass_rm_all_cmd(cJSON *root);

#ifdef __cplusplus
}
Expand Down
12 changes: 12 additions & 0 deletions sites_template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
mqtt:
host: homeassistant.local
port: 1883
username: "your_mqtt_username"
password: "your_mqtt_password"
topic: esp-ha-speech

sites:
esp32: # This is the name of the site/satellite
lights:
- "Light 1"
- "Light 2"

0 comments on commit 9f780a0

Please sign in to comment.