Skip to content

Instantly share code, notes, and snippets.

@awarecan
Last active March 14, 2024 19:26
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save awarecan/630510a9742f5f8901b5ab284c25e912 to your computer and use it in GitHub Desktop.
Save awarecan/630510a9742f5f8901b5ab284c25e912 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'
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 payload.scope for Discovery directive
scope = directive.get('payload', {}).get('scope')
if scope is None:
# token is in payload.grantee for AcceptGrant directive
scope = directive.get('payload', {}).get('grantee')
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"),
}
}
}
return json.loads(response.data.decode('utf-8'))
@hunzoo
Copy link

hunzoo commented May 28, 2019

Hi,
I used this code to test the alexa skill but i received the below error. I am not a developer so im not able to fix this issue myself. Could you kindly check and advise what needs to be changed to fix this error?

{
"errorMessage": "HTTPSConnectionPool(host='XXX.XXX.XXX', port=8123): Max retries exceeded with url: /api/alexa/smart_home (Caused by SSLError(SSLError(1, '[SSL: UNKNOWN_PROTOCOL] unknown protocol (_ssl.c:1056)')))",
"errorType": "MaxRetryError",
"stackTrace": [
" File "/var/task/lambda_function.py", line 62, in lambda_handler\n body=json.dumps(event).encode('utf-8'),\n",
" File "/var/runtime/urllib3/request.py", line 72, in request\n **urlopen_kw)\n",
" File "/var/runtime/urllib3/request.py", line 150, in request_encode_body\n return self.urlopen(method, url, **extra_kw)\n",
" File "/var/runtime/urllib3/poolmanager.py", line 323, in urlopen\n response = conn.urlopen(method, u.request_uri, **kw)\n",
" File "/var/runtime/urllib3/connectionpool.py", line 667, in urlopen\n **response_kw)\n",
" File "/var/runtime/urllib3/connectionpool.py", line 667, in urlopen\n **response_kw)\n",
" File "/var/runtime/urllib3/connectionpool.py", line 667, in urlopen\n **response_kw)\n",
" File "/var/runtime/urllib3/connectionpool.py", line 638, in urlopen\n _stacktrace=sys.exc_info()[2])\n",
" File "/var/runtime/urllib3/util/retry.py", line 398, in increment\n raise MaxRetryError(_pool, url, error or ResponseError(cause))\n"
]
}

@ronmichel
Copy link

ronmichel commented Jun 28, 2019

Hi @awarecan. Thanks for the lambda_function. However, me and some more persons (see https://community.home-assistant.io/t/alexa-proactive-mode-unable-to-link-alexa/123530 cannot link the Alexa skill to homeassistant when the “Send Alexa Events” toggle in the Skill’s Permissions is set. Without that, linking works fine, even with the client_id and client_secret in the HA config. Do you have any idea what would be the cause of this? Thanks!

@ronmichel
Copy link

Found something: see here.

In addition, if you enable the Send Alexa Events permission for the skill, your Lambda function must handle the AcceptGrant directive. If your skill does not handle this directive, account linking fails when the user attempts to enable your skill. See Authenticate a Customer to Alexa with Permissions

@ronmichel
Copy link

@awarecan: Can you update the lambda function to handle this directive? Thanks!

@awarecan
Copy link
Author

Updated per community suggestion - have not tested by myself yet.

@markbooth73
Copy link

Hi,

Tried this several times with different combinations of credentials.

My Log from HA.

Log Details (ERROR)
Logger: aiohttp.server
Source: components/alexa/capabilities.py:1478
First occurred: 13:52:58 (2 occurrences)
Last logged: 13:53:17

Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/aiohttp/web_protocol.py", line 418, in start
    resp = await task
  File "/usr/local/lib/python3.7/site-packages/aiohttp/web_app.py", line 458, in _handle
    resp = await handler(request)
  File "/usr/local/lib/python3.7/site-packages/aiohttp/web_middlewares.py", line 119, in impl
    return await handler(request)
  File "/usr/src/homeassistant/homeassistant/components/http/real_ip.py", line 39, in real_ip_middleware
    return await handler(request)
  File "/usr/src/homeassistant/homeassistant/components/http/ban.py", line 73, in ban_middleware
    return await handler(request)
  File "/usr/src/homeassistant/homeassistant/components/http/auth.py", line 127, in auth_middleware
    return await handler(request)
  File "/usr/src/homeassistant/homeassistant/components/http/view.py", line 125, in handle
    result = await result
  File "/usr/src/homeassistant/homeassistant/components/alexa/smart_home_http.py", line 120, in post
    hass, self.smart_home_config, message, context=core.Context(user_id=user.id)
  File "/usr/src/homeassistant/homeassistant/components/alexa/smart_home.py", line 39, in async_handle_message
    response = await funct_ref(hass, config, directive, context)
  File "/usr/src/homeassistant/homeassistant/components/alexa/handlers.py", line 84, in async_api_discovery
    for alexa_entity in async_get_entities(hass, config)
  File "/usr/src/homeassistant/homeassistant/components/alexa/handlers.py", line 85, in <listcomp>
    if config.should_expose(alexa_entity.entity_id)
  File "/usr/src/homeassistant/homeassistant/components/alexa/entities.py", line 285, in serialize_discovery
    for i in self.interfaces()
  File "/usr/src/homeassistant/homeassistant/components/alexa/entities.py", line 286, in <listcomp>
    if locale in i.supported_locales
  File "/usr/src/homeassistant/homeassistant/components/alexa/capabilities.py", line 205, in serialize_discovery
    capability_resources = self.capability_resources()
  File "/usr/src/homeassistant/homeassistant/components/alexa/capabilities.py", line 1478, in capability_resources
    min_value = float(self.entity.attributes[input_number.ATTR_MIN])
KeyError: 'min'

And my output from Lambda

{
  "event": {
    "payload": {
      "type": "INTERNAL_ERROR",
      "message": "500 Internal Server Error\n\nServer got itself in trouble"
    }
  }
}

@mic-user
Copy link

mic-user commented Sep 8, 2022

Refer the step by step to implement Alexa skil bridge
at
https://alexa_skill_bridge_lambda

The Checklist for your guys in the past and the future for the record.
(If you don't pass thru below checklist any of bit, you cannot get the own Alexa skill link.)

[pre-requisition]

  1. Check HA address that your own domain or dynamic DNS(DDNS) or even IP

  2. Prepare sure SSL certification that the free Let'sencrypt or free self-signed or commercial SSL cert if you would like.

  3. Set the SSL cert on your server for HTTPS. And make sure SSL properly applied.

  4. Set Alexa configuration in configuration.xml. Start simple and fill up later after link. Make problem simple for linkage.

  5. Create AWS account

  6. Choose the AWS Lambda region that N.Virginia or Oregon or EU that related to your Alexa language locale. AWS Alexa service will automatically find your Lambda region geologically by judging your Alexa locale.

I prefer make three identically same lambda in each Regions that Virginia, Oregon, EU if you need it.

  1. Just copy & paste the source code into the Lambda(s) from Matt2005 at https://gist.github.com/matt2005/744b5ef548cc13d88d0569eea65f5e5b (thanks)

  2. Create AWS Alexa Developer Account

  3. Create AWS Alexa Skill for your own HA linkage as the materials

  4. Put the endpoint address into the Alexa skill setting(as only default if you only have one, or set multiply each by check the checkbox.)

  5. Setup the Lambdas's trigger as Alexa

  6. READ carefully ALL the MATERIALS regards above to complete the section for 'Lambda' and 'Skill' both.(above are only simple summary)

  7. Stop/cut and logout the home asisstant's(by nabucase.net) cloud annual payment service from the HA settings if you have.

[To get account link(important part)]

  1. Check the AWS Lambda region that N.Virginia or Oregon or EU that related to your Alexa locale
  2. Make sure your home assistant's exposed port is 443 not 8123 which Alexa OAuth standard requirement.

so need to change your nginx's exernal open port into 443. Giveaway the 443 to HA if another your own service holding it.

  1. Make sure your network firewall that possible to reach to Home assistant by LTE or from any outside not your home Wi-Fi self.
    by test POST https://your_ha-domain_name/api/alexa/smart_home from lambda source code that any of response working. If you can see 40x on web browser, you will be fine.

Make sure below paths are accessible without any blocker(do not care about server errors if you can reach it.)

  • GET https://your_ha-domain_name/auth/authorize (OAuth standard api interface)
  • GET https://your_ha-domain_name/auth/providers (OAuth standard api interface)
  • GET https://your_ha-domain_name/manifest.json (OAuth standard api interface)
  • POST https://your_ha-domain_name/auth/login_flow (OAuth standard api interface)
  • POST https://your_ha-domain_name/api/alexa/smart_home (Alexa standard interface)
    which means that allow every URL paths in your firewall and NginX for HA.
  1. Make sure your NginX really bypassing to your HA the outside traffic.

Especially, https://your_ha-domain_name/api/alexa/smart_home paht possible to bypass all traffic.

  1. Get your final external address, and set into all BASE_URL of environment variables & every in AWS Alexa console, AWS Lambeda, NginX, etc that port 443 without ':xxxx' part for your HA address.

  2. If your SSL cert is free one, make sure your skill as in 'DEV' mode on the Alexa console where listed. If you have commercial no matter what dev or production mode.

  3. 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.
    (if you cannot, it might firewall or any network path issue that blocked to reach to HA. So go back and recheck again.)

[Wait discovery(which Automatic drive)]

  1. AFTER, account link as above, the Alexa app gose to discovery mode.
    IF you already have discovered devices list in the Alexa app by another Alexa link service,
    the app will tell you 'NO NEW device discovered' that nothing wrong since there are NO MORE NEW devices to discover.
    or
    if you are very new to link-up with Alexa, after discovery while, you can see all the devices on your Alexa app.

Walla!. All your from now.

[Addition]

  1. Do not spend your money for same services. Since this is exactly same service you have now.

[Summary]

  • Exposed 443 HA port.
  • Any SSL cert.
  • Get the external HA Address.
  • Alexa config in HA confiuration.xml
  • Lambda at right region for locale
  • Set the address into everywhere correctly like BASE_URL, Alexa console, etc.
  • Non-blocked network path to HA reside in your room.
  • Wait discovery and use.

[Q & A]

  • Is there will be extra charges/bill from AWS Lambda or Alexa Console?

Mostly zero. Since the Lambda(a bridge between Alexa system and HA) only used when you are in log-in via Alexa skill
and discovery and some only few.
By the discovered infomation(which rarely occur), Alexa communicate with HA directly in your local home network.
In common sense, monthly 5000 calls of small Lambda only make about 0.02 USD per month. That cheap enough.
it only make sevral hundres of calls. Do not worry unless your system hacked by bad guys.

  • Is there any addtional technical modification while do all above?

No and Yes.
For Alexa console and AWS Lambda code, no extra effort needed. Just copy & paste all the things and do as the materials exaclty guide you. Do no addtional thing unless you are exaxtly understand what you are doing additionally.
For your local home network(like nginx and firewalls, etc), yes. you need to do adjust your own network configuration. But you can find everythings from web. Just do google.

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