Skip to content

Instantly share code, notes, and snippets.

@matt2005
Forked from awarecan/lambda_function.py
Last active November 10, 2024 13:01
Show Gist options
  • Save matt2005/744b5ef548cc13d88d0569eea65f5e5b to your computer and use it in GitHub Desktop.
Save matt2005/744b5ef548cc13d88d0569eea65f5e5b to your computer and use it in GitHub Desktop.
Alexa Smart Home Skill Adapter for Home Assistant
"""
Copyright 2019 Jason Hu <awaregit at gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os
import json
import logging
import urllib3
_debug = bool(os.environ.get('DEBUG'))
_logger = logging.getLogger('HomeAssistant-SmartHome')
_logger.setLevel(logging.DEBUG if _debug else logging.INFO)
def lambda_handler(event, context):
"""Handle incoming Alexa directive."""
_logger.debug('Event: %s', event)
base_url = os.environ.get('BASE_URL')
assert base_url is not None, 'Please set BASE_URL environment variable'
base_url = base_url.strip("/")
directive = event.get('directive')
assert directive is not None, 'Malformatted request - missing directive'
assert directive.get('header', {}).get('payloadVersion') == '3', \
'Only support payloadVersion == 3'
scope = directive.get('endpoint', {}).get('scope')
if scope is None:
# token is in grantee for Linking directive
scope = directive.get('payload', {}).get('grantee')
if scope is None:
# token is in payload for Discovery directive
scope = directive.get('payload', {}).get('scope')
assert scope is not None, 'Malformatted request - missing endpoint.scope'
assert scope.get('type') == 'BearerToken', 'Only support BearerToken'
token = scope.get('token')
if token is None and _debug:
token = os.environ.get('LONG_LIVED_ACCESS_TOKEN') # only for debug purpose
verify_ssl = not bool(os.environ.get('NOT_VERIFY_SSL'))
http = urllib3.PoolManager(
cert_reqs='CERT_REQUIRED' if verify_ssl else 'CERT_NONE',
timeout=urllib3.Timeout(connect=2.0, read=10.0)
)
response = http.request(
'POST',
'{}/api/alexa/smart_home'.format(base_url),
headers={
'Authorization': 'Bearer {}'.format(token),
'Content-Type': 'application/json',
},
body=json.dumps(event).encode('utf-8'),
)
if response.status >= 400:
return {
'event': {
'payload': {
'type': 'INVALID_AUTHORIZATION_CREDENTIAL'
if response.status in (401, 403) else 'INTERNAL_ERROR',
'message': response.data.decode("utf-8"),
}
}
}
_logger.debug('Response: %s', response.data.decode("utf-8"))
return json.loads(response.data.decode('utf-8'))
@trilu2000
Copy link

Many thanks for the script, it is working for me since a while now without any issues.
Would it be possible to use the share for Assist information and also the alias settings from there in the future?

@rohrbrecher
Copy link

So I had similar issues like so many people here and also in some forums around the web. My internet provider at home only assigns a public IPv6 to me and so everything I tried using reverse proxy and other services like fest-ip.net didn't work.
I found a user in a german forum (heimnetz.de) who helped me to analyse the problem for over 3 days and we ultimately found a solution which works pretty nicely for me and which might also be helpful for others.

To solve the need for a public IPv4 I rented the cheapest vServer I found, which I got from Ionos and which costs 1€/month.
This server has both a public IPv4 (to communicate with AWS) as well as a publich IPv6 (to communicate with me).
I installed a tool called 6tunnel, which is super easy to configure: 6tunnel 443 public.IPv6.of.my.reverse.proxy 443
This tool now takes all the requests sent to it on port 443 and forwards them to my reverse proxy on the same port.
My reverse proxy takes the request and forwards it to my HomeAssistant at port 8123, so I didn't even have to change that.

And it works! I know that this is a similar solution like proposed by stalsma before me but with this vServer I even have the flexibility to host my reverse proxy there, if I'd want to.

@dlangamer
Copy link

after following the instructions to a letter, my issue was in cloudflare (its always dns isnt it?). i had "bot fight mode" (Security->Bots) turned on. and it was blocking any attempt at connecting the app, as well as the lambda test. as a work around under WAF -> Tools, i added AS16509 (make sure to hit enter after you type it in) as a ip access rule to allow access. seems to work, but be kinda hacky.

Man, you killed the riddle!

I had the same problem and was having trouble for over 2 weeks. It was Bot Fight and WAF. All I had to do was add the rule in waf tools releasing the Amazon ASN and everything went well. Thank you very much. Everything working perfectly. For those who don't know how to identify the ASN that is trying to connect to the Amazon instance, just go to the CloudFlare panel > Security > Events and check the connection attempts to your instance.

God bless you! :)

@ogizhelev
Copy link

For those who use this lambda for quite some time - you might use outdated python runtime in AWS , just got this notification :

We are ending support for Python 3.8 in Lambda on October 14, 2024. This follows Python 3.8 End-Of-Life (EOL) which is scheduled for October, 2024 [1].

As described in the Lambda runtime support policy [2], end of support for language runtimes in Lambda happens in several stages. Starting on October 14, 2024, Lambda will no longer apply security patches and other updates to the Python 3.8 runtime used by Lambda functions, and functions using Python 3.8 will no longer be eligible for technical support. Also, Python 3.8 will no longer be available in the AWS Console, although you can still create and update functions that use Python 3.8 via AWS CloudFormation, the AWS CLI, AWS SAM, or other tools. Starting February 28, 2025, you will no longer be able to create new Lambda functions using the Python 3.8 runtime. Starting March 31, 2025, you will no longer be able to update existing functions using the Python 3.8 runtime.

We recommend that you upgrade your existing Python 3.8 functions to the latest available Python runtime in Lambda before October 14, 2024.

You can change it to Python 3.12 - still works with no issues .
Lambda > Functions > Your lambda name
Go to Code > Runtime settings > Edit

@wolfwander
Copy link

Hi! This code is working very fine to me. I am using the CloudFlare Add-On for Home Assistant.
The only concern is that, in the first call, the skill takes almost 10 seconds to run the command but the subsequent calls takes about 1 to 3 seconds.
Is it possible to keep, at least, one connection established using the urllib3.PoolManager to speed the first call?

@YugiFanGX
Copy link

I am not quite sure, why this isn't working for my setup.
My domain has a CNAME alias pointing to a DynDNS service. Neither the direct use of the DYNDNS url works, nor the domain.
Log look like this:

"errorMessage": "HTTPSConnectionPool(host='homeassistant.example.com', port=443): Max retries exceeded with url: /api/alexa/smart_home (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f20acbc4040>: Failed to establish a new connection: [Errno -2] Name or service not known'))",

IPV6 ping failed in Cloudshell as network is not reachable... what?

@wolfwander
Copy link

I am not quite sure, why this isn't working for my setup. My domain has a CNAME alias pointing to a DynDNS service. Neither the direct use of the DYNDNS url works, nor the domain. Log look like this:

"errorMessage": "HTTPSConnectionPool(host='homeassistant.example.com', port=443): Max retries exceeded with url: /api/alexa/smart_home (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f20acbc4040>: Failed to establish a new connection: [Errno -2] Name or service not known'))",

IPV6 ping failed in Cloudshell as network is not reachable... what?

It is not possible to use IPv6 inside Amazon Skills. You must use IPv4.

@YugiFanGX
Copy link

Oh... okay. So I expect, that in my case, I need to set up a separate proxy server for ipv4 requests tunneling everything to my ipv6 address, don't I? My ISP doesn't provide me a public IPV4 due to CG-NAT / Ds lite... It is plausible....

@rohrbrecher
Copy link

Oh... okay. So I expect, that in my case, I need to set up a separate proxy server for ipv4 requests tunneling everything to my ipv6 address, don't I? My ISP doesn't provide me a public IPV4 due to CG-NAT / Ds lite... It is plausible....

Had the same issue, check my solution a few posts up.

@paoloantinori
Copy link

The reading of the SSL configuration is wrong in the script:

https://gist.github.com/matt2005/744b5ef548cc13d88d0569eea65f5e5b#file-lambda_function-py-L55

You can easily verify that in a python interactive shell:

>>> bool("False")
True

The correct line should be:

    verify_ssl =  "False" ==  os.environ.get('NOT_VERIFY_SSL')

@francescop68
Copy link

francescop68 commented Aug 12, 2024

Hi all, I'm stucked on the following point of the guide:

"Go to Alexa app or web > go to skill list > find your skill in DEV or peroper tab > click the skill you made > 'Enable' > Now login into HA(doesn't matter MFA or not) > TADA! you can see beautiful green 'successfully linked' message."

Alexa App can access to HA, HA recognize a valid userid and password but the link process provides a generic error.
Do you have an idea?

Thanks

IMG_1548 IMG_1549 IMG_1550

@YugiFanGX
Copy link

I got it working with cloudflared. Unfortunately, if I unset include_entities, no entity gets discovered. I have to manually define them in my alexa.yaml. so how can I get alexa to import ALL my devices T__T? Why is this a whitelist? It shouldn't be, right?

@rohrbrecher
Copy link

I got it working with cloudflared. Unfortunately, if I unset include_entities, no entity gets discovered. I have to manually define them in my alexa.yaml. so how can I get alexa to import ALL my devices T__T? Why is this a whitelist? It shouldn't be, right?

Believe me when I say: You don't want that. Alexa will find every single Entity, tons if them will be useless and deleting them from the Alexa app is a pita. It's way better to select the entities specifically (and fine tune their names/ categories) or to at least only select certain categories for discovery.

@YugiFanGX
Copy link

YugiFanGX commented Aug 17, 2024

Believe me when I say: You don't want that. Alexa will find every single Entity, tons if them will be useless and deleting them from the Alexa app is a pita.

Not every entity. Just those who were made available for voice assistants already (url home-assistant.xyz/config/voice-assistants/expose)... I am just curious. When I've set up a LONG_LIVED_ACCESS_TOKEN and Debug to true, then started a test discovery, I got everything I wanted... Even scenes were included..

EDIT: you were right. I will make use of the filter. thank you.

@philios33
Copy link

For those who are worried about exposing your HA to the world. I have my HA protected with mTLS (nginx reverse proxy), and I install TLS client certs/keys only on trusted devices so I can be sure only those people can get through the proxy. On Chrome it asks what client cert you want to use when you visit the url. It works with the Android Companion App, and on Chrome seamlessly, probably because it harnesses a webkit frame. You have to run your own CA and sign user certs manually, but it should be a one time thing, and devices tend to remember what certs to auth with.

So I signed a new certificate and pkey for use in the Alexa Lambda and ported this code to nodeJS 20. When proxying the POST request, I setup a fetch agent with the client credentials, so now this lambda is authorized through, and not everyone in the world. The lambda is only triggered by Alexa and not by API Gateway, so it's pretty secure unless somebody hacks your AWS account and finds the raw keys in the lambda code.

I also noticed that upon account linking, my phone was calling /auth/authorize (which worked), but the Echo Dot was trying to fetch a token (without the client creds). So I had to make a 2nd proxy lambda for the POST /auth/token call (which does a similar thing), put it on an API Gateway and replace the auth/token URL when linking accounts in the dev portal.

It was a lot of work, but it seems to be a decent setup.

@lluisd
Copy link

lluisd commented Sep 9, 2024

Hi! This code is working very fine to me. I am using the CloudFlare Add-On for Home Assistant. The only concern is that, in the first call, the skill takes almost 10 seconds to run the command but the subsequent calls takes about 1 to 3 seconds. Is it possible to keep, at least, one connection established using the urllib3.PoolManager to speed the first call?

I have the same issue, how do you solved it?

@myevit
Copy link

myevit commented Oct 2, 2024

Hi! This code is working very fine to me. I am using the CloudFlare Add-On for Home Assistant. The only concern is that, in the first call, the skill takes almost 10 seconds to run the command but the subsequent calls takes about 1 to 3 seconds. Is it possible to keep, at least, one connection established using the urllib3.PoolManager to speed the first call?

I have the same issue, how do you solved it?

Same

@lluisd
Copy link

lluisd commented Oct 2, 2024

Hi! This code is working very fine to me. I am using the CloudFlare Add-On for Home Assistant. The only concern is that, in the first call, the skill takes almost 10 seconds to run the command but the subsequent calls takes about 1 to 3 seconds. Is it possible to keep, at least, one connection established using the urllib3.PoolManager to speed the first call?

I have the same issue, how do you solved it?

Same

I solved it. In my case I have the firewall which I allow Ireland country where I run my AWS Lambda. But in some cases it uses some ip which is not listed in the country list of Synology firewall, so I added the problematic ranges manually:

54.240.197.0/24
54.239.99.0/24

I checked it in HA by user > security > tokens update list where I can see some of the ips used on login to layla.amazon.com (EU) and check the ranges from AWS https://ip-ranges.amazonaws.com/ip-ranges.json and add them if my Synology Ireland country list didn't take that range into account. After that no problems anymore of trying 2-3 times call Alexa. I also have that problem on activating the skill in Alexa where I always need to disable the firewall to authenticate to my HA.

@wolfwander mention

@wolfwander
Copy link

Hi! This code is working very fine to me. I am using the CloudFlare Add-On for Home Assistant. The only concern is that, in the first call, the skill takes almost 10 seconds to run the command but the subsequent calls takes about 1 to 3 seconds. Is it possible to keep, at least, one connection established using the urllib3.PoolManager to speed the first call?

I have the same issue, how do you solved it?

Same

I moved to Matterbridge Add-On. Much faster but a little bit buggy...
I installed two instances of Matterbridge:

  • one in the same machine of HASS to expose entities with an specific label
  • one in the same machine of Zigbee2MQTT to expose Z2M devices

@myevit
Copy link

myevit commented Oct 4, 2024

Hi! This code is working very fine to me. I am using the CloudFlare Add-On for Home Assistant. The only concern is that, in the first call, the skill takes almost 10 seconds to run the command but the subsequent calls takes about 1 to 3 seconds. Is it possible to keep, at least, one connection established using the urllib3.PoolManager to speed the first call?

I have the same issue, how do you solved it?

Same

I moved to Matterbridge Add-On. Much faster but a little bit buggy... I installed two instances of Matterbridge:

  • one in the same machine of HASS to expose entities with an specific label
  • one in the same machine of Zigbee2MQTT to expose Z2M devices

That's awesome tip!!!!! Thanks!!!!

@Sonorc
Copy link

Sonorc commented Oct 29, 2024

Hi Guys, i have a little Problem. I have a DS-Lite Connection at home so i'm using AAAA Records to remote-Access my Home-Assistant via IPV6. But unfortunately this Code doesn't work for me for some reasons... I can normally access my HomeAssistant and i even get a "405" when entering https://YOUR-HA-URL:YOUR-PORT/api/alexa/smart_home , wherever i am. But why can't the Lambda function conntect to my HA... error message as follows: error msg

I hope someone can help me... don't know what to do.. or is the Lambda Function just unable to do ipv6 connections? Thanks in advance!

Hi Guys, i have a little Problem. I have a DS-Lite Connection at home so i'm using AAAA Records to remote-Access my Home-Assistant via IPV6. But unfortunately this Code doesn't work for me for some reasons... I can normally access my HomeAssistant and i even get a "405" when entering https://YOUR-HA-URL:YOUR-PORT/api/alexa/smart_home , wherever i am. But why can't the Lambda function conntect to my HA... error message as follows: error msg
I hope someone can help me... don't know what to do.. or is the Lambda Function just unable to do ipv6 connections? Thanks in advance!

Did you find any solution?

Hello

@GamingMaker
@mandarBadve

Did you find any solution?

@stalsma
Copy link

stalsma commented Oct 29, 2024

@Sonorc The lamba will not hit your IP6 addresss. It will only resolve IP4 addresses. You can use CloudFlare (no cost) to get an IP4 address (on a CloudFlare server) that you then tunnel into your IP6 host.

I finally got the Alexa account to link by:

  1. setup a cloudflare tunnel (gives me the public IP4 addr) that
  2. tunnels to a CNAME entry maintained by DDNS (Synology's built-in service supports IP6)
  3. CNAME IP resolves to my NAS IP directly (router forwards port 443 is to NAS)
  4. A reverse proxy (Synology Login Portal) translates the incoming address (https://my-cloudflare-domain:443) to http://localhost:8123/.

@Sonorc
Copy link

Sonorc commented Oct 30, 2024

@stalsma thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment