|
""" |
|
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, json, logging, urllib3, configparser, boto3, traceback |
|
|
|
_debug = bool(os.environ.get('DEBUG')) |
|
|
|
_logger = logging.getLogger('HomeAssistant-SmartHome') |
|
_logger.setLevel(logging.DEBUG if _debug else logging.INFO) |
|
|
|
# Initialize boto3 client at global scope for connection reuse |
|
client = boto3.client('ssm') |
|
app_config_path = os.environ['APP_CONFIG_PATH'] |
|
# Initialize app at global scope for reuse across invocations |
|
app = None |
|
|
|
class HA_Config: |
|
def __init__(self, config): |
|
""" |
|
Construct new app with configuration |
|
:param config: application configuration |
|
""" |
|
self.config = config |
|
|
|
def get_config(self): |
|
return self.config |
|
|
|
def load_config(ssm_parameter_path): |
|
""" |
|
Load configparser from config stored in SSM Parameter Store |
|
:param ssm_parameter_path: Path to app config in SSM Parameter Store |
|
:return: ConfigParser holding loaded config |
|
""" |
|
configuration = configparser.ConfigParser() |
|
try: |
|
# Get all parameters for this app |
|
param_details = client.get_parameters_by_path( |
|
Path=ssm_parameter_path, |
|
Recursive=False, |
|
WithDecryption=True |
|
) |
|
|
|
# Loop through the returned parameters and populate the ConfigParser |
|
if 'Parameters' in param_details and len(param_details.get('Parameters')) > 0: |
|
for param in param_details.get('Parameters'): |
|
param_path_array = param.get('Name').split("/") |
|
section_position = len(param_path_array) - 1 |
|
section_name = param_path_array[section_position] |
|
config_values = json.loads(param.get('Value')) |
|
config_dict = {section_name: config_values} |
|
configuration.read_dict(config_dict) |
|
except BaseException as err: |
|
print("Encountered an error loading config from SSM.") |
|
print(str(err)) |
|
finally: |
|
return configuration |
|
|
|
def lambda_handler(event, context): |
|
global app |
|
|
|
# Initialize app if it doesn't yet exist |
|
if app is None: |
|
print("Loading config and creating persistence object...") |
|
config = load_config(app_config_path) |
|
app = HA_Config(config) |
|
|
|
appConfig = app.get_config()['appConfig'] |
|
|
|
base_url = appConfig['HA_BASE_URL'] |
|
cf_client_id = appConfig['CF_CLIENT_ID'] |
|
cf_client_secret = appConfig['CF_CLIENT_SECRET'] |
|
|
|
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 = appConfig['HA_TOKEN'] # only for debug purpose |
|
|
|
verify_ssl = not bool(os.environ.get('NOT_VERIFY_SSL')) |
|
|
|
"""Handle incoming Alexa directive.""" |
|
_logger.debug('Event: %s', event) |
|
|
|
assert base_url is not None, 'Please set BASE_URL environment variable' |
|
base_url = base_url.strip("/") |
|
|
|
_logger.debug("Base url: {}".format(base_url)) |
|
|
|
directive = event.get('directive') |
|
assert directive is not None, 'Malformatted request - missing directive' |
|
assert directive.get('header', {}).get('payloadVersion') == '3', \ |
|
'Only support payloadVersion == 3' |
|
|
|
http = urllib3.PoolManager( |
|
cert_reqs='CERT_REQUIRED' if verify_ssl else 'CERT_NONE', |
|
timeout=urllib3.Timeout(connect=2.0, read=10.0) |
|
) |
|
|
|
api_path = '{}/api/alexa/smart_home'.format(base_url) |
|
|
|
response = http.request( |
|
'POST', |
|
api_path, |
|
headers={ |
|
'Authorization': 'Bearer {}'.format(token), |
|
'Content-Type': 'application/json', |
|
'CF-Access-Client-Id': cf_client_id, |
|
'CF-Access-Client-Secret': cf_client_secret, |
|
}, |
|
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 {}".format(response.status), |
|
'message': response.data.decode("utf-8"), |
|
} |
|
} |
|
} |
|
_logger.debug('Response: %s', response.data.decode("utf-8")) |
|
return json.loads(response.data.decode('utf-8')) |
@dkaser Thanks for such a guide, clear described steps and very helpful. However just observed one issue that is adding devices to Routines in Alexa app.
1.) I am able to add to Alexa and control in Alexa my lights which are exposed from HA
2.) The issue is when I try to add routines, there are no devices i.e. More -> Routines -> + -> Add action -> Smart Home -> there are no devices (not even the ones I see in the Alexa app dashboard)
Do you observe the same issue ? Can you utilize added devices in Alexa and create routines with this devices ?
Thanks
EDIT: I found out that I needed to create at least 1 group in Devices, like kitchen, and then all the devices will show up in the routines. When I remove this group, the devices disappear again (but devices across defined routines are kept)... Very strange behaviour, but may help others, you need at least one group.