OK, here we go. This is what's needed to push Domoticz state changes for door/window contacts to be usable in Alexa routines. Make sure you have the
https://github.com/rimram31/dz_smarthome/tree/dev dev branch of Alexicz from Github which is where work on async reporting started by RimRam31. Note you will need to re-do account linking for Alexicz when all done.
First, go to the Alexa Developer Console and under the 'Build' tab for Alexicz, go to the permisisons section and select 'Send Alexa Events'. Note that when you do that, account linking will always fail unless you properly implement the AcceptGrant response (next section). So if you do anything wrong later on and cannot link your account, go back to this portal and turn this option back off again. That's important to remember !
Take note of the Alexa Client ID and Alexa Client secret on this permission page. And remember, they are different to the LWA client ID/secret used for account linking itself.
Add the following class to the AlexaSmartHome.py file to respond to the AcceptGrant directive which is sent to the skill when account linking completes with event permissions. I added it just under class Discovery(AlexaSmartHomeCall) inside class Alexa(object).
Code: Select all
class Authorization(AlexaSmartHomeCall):
def AcceptGrant(self, directive):
print('AcceptGrant: ', directive)
try:
if 'grantee' in directive['payload']:
self.handler.setAccessToken(directive['payload']['grantee']['token'])
except Exception: pass
response = {
'event': {
'header': {
'namespace': 'Alexa.Authorization',
'name': 'AcceptGrant.Response',
'payloadVersion': '3',
'messageId': str(uuid4())
},
'payload': {}
}
}
return response
Now, when you account link the skill, the CloudWatch logs will show an entry with the AcceptGrant request and the payload will contain a short code that looks something like RHwmsruJWOeBfqHdFEfN. Note that it's only valid for about 5 minutes and you get one shot with it - if you make any mistakes later on, you'll have to disable the skill, redo account linking, get a new code. Rinse and repeat. We will exchange that short code for the much longer Atza|blah-blah codes that are needed for the async change reports using a script called from Domoticz.
Upload that modified AlexaSmartHome.py file to the Lambda function for Alexicz.
Now create a totally new python file called reportstate.py which will be run on the machine your Domoticz is on and NOT in the Lambda. This is the script you will execute from the on/off script actions - with the IDX of the triggering device - to send the events from Domoticz to the Alexa gateway.
EDIT: THERE'S A BETTER VERSION OF THIS SCRIPT A FEW POSTS DOWN !
Code: Select all
#!/usr/bin/env python3
# (c) philchillbill, 2019. No rights reserved.
import sys, time, datetime, json, uuid, os
import requests
import AlexaSmartHome, DomoticzHandler
ALEXA_URI = "https://api.eu.amazonalexa.com/v3/events"
CODE = "RHwmsruJWOeBfqHdFEfN"
CLIENT_ID = "the.client.id.from.the.permissions.for.send.alexa.events"
CLIENT_SECRET = "the.client.secret.from.the.permissions.for.send.alexa.events"
REDIRECT_URI = "https://www.amazon.com/ap/oa/?redirect_url=https://layla.amazon.com/api/skill/link/xxxxxxxxxxxxxxxxxx"
LOCAL_DOMOTICZ_IP = "http://192.168.178.12:8080"
PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300
PATH = os.path.dirname(os.path.abspath(__file__))
TOKEN_FILENAME = CODE + ".txt"
UTC_FORMAT = "%Y-%m-%dT%H:%M:%S.00Z"
LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token"
LWA_HEADERS = { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }
def get_utc_timestamp(seconds=None):
return time.strftime(UTC_FORMAT, time.gmtime(seconds))
def get_utc_timestamp_from_string(string):
return datetime.datetime.strptime(string, UTC_FORMAT)
def get_uuid():
return str(uuid.uuid4())
def get_need_new_token():
"""Checks whether the access token is missing or needed to be refreshed"""
need_new_token_response = {
"need_new_token": False,
"access_token": "",
"refresh_token": ""
}
if os.path.isfile(TOKEN_FILENAME):
with open(TOKEN_FILENAME, 'r') as infile:
last_line = infile.readlines()[-1]
token = last_line.split("***")
parts = len(token)
# sometimes we get extra or zero LFs in the last line so compensate
if parts == 3:
real_last_line = last_line.split("}")[1] + "}"
token = real_last_line.split("***")
token_received_datetime = get_utc_timestamp_from_string(token[0])
token_json = json.loads(token[1])
token_expires_in = token_json["expires_in"] - PREEMPTIVE_REFRESH_TTL_IN_SECONDS
token_expires_datetime = token_received_datetime + datetime.timedelta(seconds=token_expires_in)
current_datetime = datetime.datetime.utcnow()
need_new_token_response["need_new_token"] = current_datetime > token_expires_datetime
need_new_token_response["access_token"] = token_json["access_token"]
need_new_token_response["refresh_token"] = token_json["refresh_token"]
else:
need_new_token_response["need_new_token"] = True
return need_new_token_response
def get_access_token():
"""Performs access token or token refresh request as needed and returns valid access token"""
need_new_token_response = get_need_new_token()
access_token = ""
if need_new_token_response["need_new_token"]:
if os.path.isfile(TOKEN_FILENAME):
with open(TOKEN_FILENAME, 'a') as outfile:
outfile.write("\n")
lwa_params = {
"grant_type" : "refresh_token",
"refresh_token": need_new_token_response["refresh_token"],
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": REDIRECT_URI
}
else:
lwa_params = {
"grant_type" : "authorization_code",
"code": CODE,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": REDIRECT_URI
}
response = requests.post(LWA_TOKEN_URI, headers=LWA_HEADERS, data=lwa_params, allow_redirects=True)
if response.status_code != 200:
return None
token = get_utc_timestamp() + "***" + response.text
with open(TOKEN_FILENAME, 'a') as outfile:
outfile.write(token)
access_token = json.loads(response.text)["access_token"]
else:
access_token = need_new_token_response["access_token"]
return access_token
def main(argv):
token = get_access_token()
device_id = argv[0]
portal = DomoticzHandler.Domoticz(LOCAL_DOMOTICZ_IP)
if device_id != '':
device = portal.getDevice(device_id)
endpoint = portal.getEndpointDevice(device)
data = AlexaSmartHome.api_reportState(endpoint, token)
headers = { 'Content-Type': 'application/json' }
headers['Authorization'] = 'Bearer ' + token
response = requests.post(ALEXA_URI, headers=headers, json=data)
print(response)
if __name__ == "__main__":
main(sys.argv[1:])
You will need to edit the parameters at the top of the file to match your setup:
ALEXA_URI = "
https://api.eu.amazonalexa.com/v3/events"
CODE = the short code you get from the AcceptGrant directive above
CLIENT_ID = from Developer Portal
CLIENT_SECRET = from Developer Portal
REDIRECT_URI = from Developer Portal, under account linking
LOCAL_DOMOTICZ_IP = your local Domoticz instance, with port number
Make sure to place a copy of the AlexaSmartHome.py and DomoticzHandler.py files in the same directory as this script so it can import them.
Test that it works by doing chmod +x and typing e.g. ./reportstate.py 379 (where 379 is the Domoticz IDX of a door/window sensor). That sensor will now be usable in a routine in the Alexa app. If all is ok, trigger it from now on with "e.g. script://reportstate.py 123" in your Domoticz devices script fields.
Enjoy. If anybody feels like making this a plugin for Domoticz, be my guest !