Skip to content

Instantly share code, notes, and snippets.

@matt2005
Forked from awarecan/lambda_function.py
Last active December 26, 2025 06:24
Show Gist options
  • Select an option

  • Save matt2005/744b5ef548cc13d88d0569eea65f5e5b to your computer and use it in GitHub Desktop.

Select an option

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>
Modified 2020 Matthew Hilton <[email protected]>
Refactor and Modernised 2025 Matthew Hilton <[email protected]>
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 json
import logging
import logging.handlers
import os
from typing import Any
import urllib3
# Configure debug mode
_debug = bool(os.environ.get('DEBUG'))
# Configure logging with enhanced formatting
_log_format = '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
_formatter = logging.Formatter(_log_format)
# Console handler
_console_handler = logging.StreamHandler()
_console_handler.setFormatter(_formatter)
# Logger setup
_logger = logging.getLogger('HomeAssistant-SmartHome')
_logger.setLevel(logging.DEBUG if _debug else logging.INFO)
_logger.addHandler(_console_handler)
# Suppress debug logs from urllib3
logging.getLogger('urllib3').setLevel(logging.INFO)
def lambda_handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
"""Handle incoming Alexa directive.
Args:
event: The Alexa directive event payload
context: AWS Lambda context object
Returns:
Response payload for Alexa
Raises:
AssertionError: If request validation fails
"""
_logger.info('Processing Alexa request')
_logger.debug('Event payload: %s', json.dumps(event, indent=2))
try:
base_url = os.environ.get('BASE_URL')
if base_url is None:
_logger.error('BASE_URL environment variable not set')
raise ValueError('BASE_URL environment variable must be set')
base_url = base_url.rstrip('/')
_logger.debug('Base URL: %s', base_url)
directive = event.get('directive')
if directive is None:
_logger.error('Malformed request: missing directive')
raise ValueError('Request missing required directive field')
payload_version = directive.get('header', {}).get('payloadVersion')
if payload_version != '3':
_logger.error('Unsupported payloadVersion: %s', payload_version)
raise ValueError(f'Only payloadVersion 3 is supported, got {payload_version}')
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')
if scope is None:
_logger.error('Malformed request: missing scope/token')
raise ValueError('Request missing scope in endpoint or payload')
scope_type = scope.get('type')
if scope_type != 'BearerToken':
_logger.error('Unsupported scope type: %s', scope_type)
raise ValueError(f'Only BearerToken scope is supported, got {scope_type}')
token = scope.get('token')
if token is None and _debug:
_logger.debug('Token not found in request, using LONG_LIVED_ACCESS_TOKEN from environment')
token = os.environ.get('LONG_LIVED_ACCESS_TOKEN')
if token is None:
_logger.error('No authentication token available')
raise ValueError('Authentication token is required')
verify_ssl = not bool(os.environ.get('NOT_VERIFY_SSL'))
_logger.debug('SSL verification enabled: %s', verify_ssl)
http = urllib3.PoolManager(
cert_reqs='CERT_REQUIRED' if verify_ssl else 'CERT_NONE',
timeout=urllib3.Timeout(connect=2.0, read=10.0)
)
_logger.info('Sending request to Home Assistant')
response = http.request(
'POST',
f'{base_url}/api/alexa/smart_home',
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json',
},
body=json.dumps(event).encode('utf-8'),
)
_logger.debug('Response status: %s', response.status)
if response.status >= 400:
response_text = response.data.decode('utf-8')
_logger.error('Home Assistant returned error %s: %s', response.status, response_text)
error_type = 'INVALID_AUTHORIZATION_CREDENTIAL' if response.status in (401, 403) else 'INTERNAL_ERROR'
return {
'event': {
'payload': {
'type': error_type,
'message': response_text,
}
}
}
response_data = json.loads(response.data.decode('utf-8'))
_logger.info('Successfully processed Alexa request')
_logger.debug('Response: %s', json.dumps(response_data, indent=2))
return response_data
except (ValueError, KeyError, json.JSONDecodeError) as e:
_logger.exception('Error processing request: %s', str(e))
return {
'event': {
'payload': {
'type': 'INVALID_REQUEST',
'message': str(e),
}
}
}
except Exception as e:
_logger.exception('Unexpected error: %s', str(e))
return {
'event': {
'payload': {
'type': 'INTERNAL_ERROR',
'message': 'An unexpected error occurred',
}
}
}
@David-Kopczynski
Copy link

David-Kopczynski commented Feb 13, 2025

@pascal260303, I am also facing issues regarding the account linking... I have tried various combinations of AWS locations, skill languages, client ids, etc. and also moved away from cloudflare proxies. I can see that the Discovery example is working fine, but during the linking process I get a blank page with the URL being external/link-result?success=false. Would love to know how to debug this

Edit: Just updated the Alexa app. However, instead of a blank page I get "Alexa couldn't find a new device to connect."

@mfaugiana
Copy link

mfaugiana commented Mar 28, 2025

With my setup, which has a public static IP address, Nginx add-on, Let's Encrypt ( NO Cloudflare), the integration works only with port 443.

Fortunately, there is another solution that works with a custom SSL port.
https://indomus.it/guide/integrare-gratuitamente-amazon-echo-alexa-con-home-assistant-via-haaska-e-aws/

Personally speaking, I did this way years ago, and it worked until I decided to try the new way according to the official documentation, and I ended up losing some days.

Official documentation should underline that only 443 port MUST be used.

I hope this helps others.

@ballakers
Copy link

ballakers commented Mar 29, 2025 via email

@lluisd
Copy link

lluisd commented Apr 1, 2025

every 4 days I have these log messages with Amazon IPs and I don't know exactly if it's related to that script, but everything it's working fine. I also use the Ireland endpoint for European accounts, I have a geoip blocker for non Ireland ips and these ones seems from USA, I don't know how to figure out if it's comming from AWS, alexa developer console or also can be related to Alexa Media Player which is not related to that but I don't expect.

[homeassistant] 2025-03-31 08:01:44.685 WARNING (MainThread) [homeassistant.components.http.ban] Login attempt or request with invalid authentication from 72.21.217.139 (72.21.217.139). Requested URL: '/auth/token'. (Apache-HttpClient/UNAVAILABLE (Java/1.8.0_432))
	
[homeassistant] 2025-03-27 06:35:33.106 WARNING (MainThread) [homeassistant.components.http.ban] Login attempt or request with invalid authentication from 72.21.217.98 (72.21.217.98). Requested URL: '/auth/token'. (Apache-HttpClient/UNAVAILABLE (Java/1.8.0_432))
	
[homeassistant] 2025-03-23 10:56:06.647 WARNING (MainThread) [homeassistant.components.http.ban] Login attempt or request with invalid authentication from 54.239.98.30 (54.239.98.30). Requested URL: '/auth/token'. (Apache-HttpClient/UNAVAILABLE (Java/1.8.0_432))
	
[homeassistant] 2025-03-19 12:47:52.931 WARNING (MainThread) [homeassistant.components.http.ban] Login attempt or request with invalid authentication from 72.21.217.18 (72.21.217.18). Requested URL: '/auth/token'. (Apache-HttpClient/UNAVAILABLE (Java/1.8.0_432))
	
[homeassistant] 2025-03-15 11:33:52.312 WARNING (MainThread) [homeassistant.components.http.ban] Login attempt or request with invalid authentication from 72.21.217.143 (72.21.217.143). Requested URL: '/auth/token'. (Apache-HttpClient/UNAVAILABLE (Java/1.8.0_432))
	
[homeassistant] 2025-03-11 12:38:33.360 WARNING (MainThread) [homeassistant.components.http.ban] Login attempt or request with invalid authentication from 54.239.98.38 (54.239.98.38). Requested URL: '/auth/token'. (Apache-HttpClient/UNAVAILABLE (Java/1.8.0_432))
	
[homeassistant] 2025-03-07 18:31:23.417 WARNING (MainThread) [homeassistant.components.http.ban] Login attempt or request with invalid authentication from 72.21.217.46 (72.21.217.46). Requested URL: '/auth/token'. (Apache-HttpClient/UNAVAILABLE (Java/1.8.0_432))

@DarkPatch
Copy link

AWS has revised their Lambda Function URL Authorization.

We are reaching out because AWS Lambda is making changes to the Lambda function URL authorization model to improve the security posture of function URLs. Function URLs allow you to easily configure a dedicated HTTP(S) endpoint for your Lambda function. The new authorization model requires your permissions policies to include both lambda:InvokeFunctionUrl and lambda:InvokeFunction actions [1]. Previously, only lambda:InvokeFunctionUrl permissions were required.

Has anyone implemented these changes?

I've gone through the process to create an IAM Role and User, as wells as adding permission to do lambd:InvokeFunctionURL.
So far, the only thing I've found is we can create a function URL and ignore the AWS_IAM instead using None, which should rely on authentication via the Lambda function.

Is it possible to use the AWS_IAM mode?

@mcrespov
Copy link

mcrespov commented Nov 3, 2025

Hi, my server is behind a Cloudflare proxy, and since this afternoon I haven’t been able to validate my skill... It’s been working for over a year, and without changing anything, after entering my HA credentials (which are correct), the Alexa app says: “Unable to link your account with Alexa, please try again later.”

Is anyone else experiencing this issue?

@dwmw2
Copy link

dwmw2 commented Nov 4, 2025

Can't you extract the BASE_URL from the auth token? Like I do for Domoticz in https://github.com/dwmw2/alexa_domo/blob/master/domo-code/domoticz.js#L15-L44

@atzel48
Copy link

atzel48 commented Nov 6, 2025

Hi, my server is behind a Cloudflare proxy, and since this afternoon I haven’t been able to validate my skill... It’s been working for over a year, and without changing anything, after entering my HA credentials (which are correct), the Alexa app says: “Unable to link your account with Alexa, please try again later.”

Is anyone else experiencing this issue?

Go to Cloudflare and navigate to Your_Account > Your_Hostname > Security > Settings, then disable “Bot Fight Mode.” You lose a bit of security, but it's the only way that works for me until I find a better solution.

@matt2005
Copy link
Author

I've refactored some of the lambda and tidyed it up as I need to update to the python 3.14 runtime.
The logging is now improved and the code flow works better.

@TomHeinemeyer
Copy link

I've refactored some of the lambda and tidyed it up as I need to update to the python 3.14 runtime. The logging is now improved and the code flow works better.

Care to share? :)

@matt2005
Copy link
Author

I updated the gist a few days ago.

@TomHeinemeyer
Copy link

I updated the gist a few days ago.

Oh, I didn't see that it's your gist, thank you!

@danielbrunt57
Copy link

danielbrunt57 commented Dec 26, 2025

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.

Are we still limited to just these filter options?

  • include_domains: Exposes all entities belonging to specified domains (e.g., light, switch, climate).
  • exclude_domains: Hides all entities belonging to specified domains.
  • include_entities: Exposes only the explicitly listed entities (e.g., light.living_room_lights). If this is the only filter type used, only those entities will be included.
  • exclude_entities: Hides specific entities, even if their domain is included.
  • include_entity_globs: Exposes entities that match a specified pattern using wildcards (globs), such as sensor.*_temperature.
  • exclude_entity_globs: Hides entities that match a specified pattern using wildcards.

It would be nice to be able to leverage home-assistant.xyz/config/voice-assistants/expose or be able to include/exclude labels...

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