Actionable Notifications via Alexa Media Player

@wilson87j thanks for your postings. I tried this for weeks without success. All I got was this “I can’t do this” error from Alexa. So I started from scratch today.

And I had at least one of your mistakes. On the first page “create your skill” I missed the dropdown for the region. On default its set to “US East” which did not work for me in Europe/Germany.
I had to choose EU Ireland.

The second thing what is a little confusing is the language JSON. In the GitHub Wiki there is a locale_de_de.json with exact the same content. In my first try, I uploaded it, but got some errors. Now I only uploaded the locale_en_us.json and everything works fine. Even in German.

Everything else what could be set up in the alexa dev panel could be ignored.

But I think the biggest facepalm was my firewall. I totally forgot, that I had blocked all incoming traffic from outside Germany.

Alexa is trying to call my HA from several IPs. Most of them from Ireland and UK, but strangely also from Japan. On pfSsense with pfBlockerNG I had to manually remove them from the blacklist.

Now everything works as written in the wiki. What a fun. :slight_smile:

Hi All,

I follow the steps very clear. My HA is behind Nginx Proxy Manager
I can connect to HA using:

https://ha.domain.com (Proxy will forward this to 192.168.100.24:8123)

Login attempt or request with invalid authentication from a0d7b954-nginxproxymanager.local.hass.io (172.30.33.7). See the log for details.

When I give a LONG LIFE TOKEN and disable the account linking. Then I get this:

Sorry I did not catch that... None

sorry for the late reply, i’ve only just seen this. glad i could be some sort of help and you managed to get it working! :slight_smile:

1 Like

That’s how I have my setup too. So you can work with account linking.
Does the account linking process works, if you want to add the skill to your account?

Yes the account linking is working. But im behind the Nginx Proxy Manager
I see in the log of Nginx Proxy Manager some errors

https://myhomeassistant.com/auth/authorize not found

followed the instructions and got everything setup and this happens:

I say Alexa, open custom actions…“welcome to the conversation start sample…what’s your favorite color”?

“what’s your favorite color”

This happened to me too… It’s because we selected the wrong template. I saw the hello world in the conversation template and picked it. When actually needed to pick the Start from Scratch template

** TO BE CLEAR ** pick the “Start from Scratch” template, top left, not the bottom one that I highlighted “why” I picked it by accident.

great minds think alike! I caught that this morning and deleted everything and re-created under Start From Scratch…so NOW when I say “Alexa, open custom actions” it does respond, which is great, but it responds with the ASK I’m using in Node Red.

So now my only issue is when I set it to trigger off an event in Node Red, like Kitchen lights ON, it doesnt actually ask/say anything over Alexa…it doesnt make the jump from the Ask node to the Alexa Actionable Notification node…

image

Sorry I don’t think I’ll be able to help on the Node-Red part, I’ve only tested this from the home assistant developer tools

image

anyone successfully using Node Red for this?

I have it all set up according to the examples, I can see it call the Script, but nothing ever comes over Alexa AND no event is received

here’s another example where the service call script seemingly works (look at timestamps) but the event is never received by the Events node and Alexa never speaks

Works perfect for me.

Could be a tiepo (typo) :upside_down_face: somewhere. Looks like you are very close.

This does not work.

But this does.

Check, double check even triple check ALL your nodes.

This works for me.

[{"id":"ebaa5806.f6b05","type":"api-call-service","z":"ed139e56.be3498","name":"Turn kitchen lights off","server":"ceea5ac5.1b1658","version":1,"debugenabled":false,"service_domain":"switch","service":"turn_off","entityId":"switch.kitchen_2_lights","data":"","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":760,"y":160,"wires":[["70bb75f9.645b64"]]},{"id":"b8fa98ba.a2624","type":"switch","z":"ed139e56.be3498","name":"Response","property":"payload.event.event_response","propertyType":"msg","rules":[{"t":"eq","v":"ResponseYes","vt":"str"},{"t":"eq","v":"ResponseNo","vt":"str"},{"t":"eq","v":"ResponseNone","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":390,"y":180,"wires":[["ebaa5806.f6b05"],["92f1292c.ae16a"],["db745314.7c4988"]]},{"id":"c7edd154.20e698","type":"switch","z":"ed139e56.be3498","name":"Event ID","property":"payload.event.event_id","propertyType":"msg","rules":[{"t":"eq","v":"kitchen","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":180,"y":180,"wires":[["b8fa98ba.a2624"]]},{"id":"30c7619f.e7b9e6","type":"server-events","z":"ed139e56.be3498","name":"Notification Response","server":"ceea5ac5.1b1658","event_type":"alexa_actionable_notification","exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"waitForRunning":true,"x":120,"y":260,"wires":[["c7edd154.20e698"]]},{"id":"1ced62f8.f79405","type":"server-state-changed","z":"ed139e56.be3498","name":"Kitchen lights on ?","server":"ceea5ac5.1b1658","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"switch.kitchen_2_lights","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"on","halt_if_type":"str","halt_if_compare":"is","outputs":2,"output_only_on_state_change":false,"for":"","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"x":130,"y":340,"wires":[["db745314.7c4988"],[]]},{"id":"db745314.7c4988","type":"trigger","z":"ed139e56.be3498","name":"On for 1 minute ?","op1":"","op2":"","op1type":"nul","op2type":"date","duration":"60","extend":true,"overrideDelay":false,"units":"s","reset":"ON","bytopic":"all","topic":"topic","outputs":1,"x":430,"y":340,"wires":[["e93fd9a9.067d3"]]},{"id":"c3172e73.b27508","type":"api-call-service","z":"ed139e56.be3498","name":"Alexa kitchen notification","server":"ceea5ac5.1b1658","version":1,"debugenabled":true,"service_domain":"script","service":"activate_alexa_actionable_notification","entityId":"","data":"{\"text\":\" kitchen lights are on for 1 minute. Would you like me to turn them off?\",\"event_id\":\"kitchen\",\"alexa_device\":\"media_player.living_room_echo_dot\"}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":890,"y":340,"wires":[["70bb75f9.645b64"]]},{"id":"e93fd9a9.067d3","type":"api-current-state","z":"ed139e56.be3498","name":"Is light on?","server":"ceea5ac5.1b1658","version":1,"outputs":2,"halt_if":"on","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"switch.kitchen_2_lights","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":670,"y":320,"wires":[["c3172e73.b27508"],[]]},{"id":"70bb75f9.645b64","type":"debug","z":"ed139e56.be3498","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1110,"y":220,"wires":[]},{"id":"92f1292c.ae16a","type":"api-call-service","z":"ed139e56.be3498","name":"📢 Whispering Voice","server":"be196a96.e67d88","version":1,"debugenabled":false,"service_domain":"notify","service":"alexa_media_living_room_echo_dot","entityId":"","data":"{\"message\":\"<amazon:effect name='whispered'>Kitchen lights are still on.</amazon:effect>\",\"data\":{\"type\":\"announce\",\"method\":\"speak\"},\"target\":[\"media_player.living_room_echo_dot\"]}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":700,"y":240,"wires":[["70bb75f9.645b64"]]},{"id":"ceea5ac5.1b1658","type":"server","name":"Home Assistant","addon":true},{"id":"be196a96.e67d88","type":"server","name":"Home Assistant","legacy":false,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]

You’ll want to change the 1 minute part once you get it going. :+1:

Hi All,

Im trying for months to let this work… But till now I didn’t solve this puzzle.

My account on Amazon - UK
Echo 1 - Language ENG US
Alexa Media Player: 3.8.2
HA Version (docker) debian: 2021.3.4

Account linking is off. Because when I use this I get error:
Error: invalid client id or redirect url.
When I try again HA will ban my NPM IP so I cant login again.
Need remove the IP from the ban list and reboot HA.

Flashbriefing and haaska skills are both working without problems (both Language UK)

Can someone help me to solve this puzzle?

Custom Skil created with UK and US Language

input_text:

input_text:
  alexa_actionable_notification:
    name: Alexa Actionable Notification Holder
    max: 255
    initial: '{"text": "This is a test of the alexa actions custom skill. Did it work?", "event": "actionable.skill.test"}'

JSON Editor:

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "custom actions",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.YesIntent",
                    "samples": [
                        "yes"
                    ]
                },
                {
                    "name": "AMAZON.NoIntent",
                    "samples": [
                        "no"
                    ]
                },
                {
                    "name": "Select",
                    "slots": [
                        {
                            "name": "Selections",
                            "type": "Selections"
                        }
                    ],
                    "samples": [
                        "{Selections}"
                    ]
                },
                {
                    "name": "Number",
                    "slots": [
                        {
                            "name": "Numbers",
                            "type": "AMAZON.FOUR_DIGIT_NUMBER"
                        }
                    ],
                    "samples": [
                        "{Numbers}"
                    ]
                },
                {
                    "name": "Duration",
                    "slots": [
                        {
                            "name": "Durations",
                            "type": "AMAZON.DURATION"
                        }
                    ],
                    "samples": [
                        "{Durations}"
                    ]
                },
                {
                    "name": "Date",
                    "slots": [
                        {
                            "name": "Dates",
                            "type": "AMAZON.DATE"
                        },
                        {
                            "name": "Times",
                            "type": "AMAZON.TIME"
                        }
                    ],
                    "samples": [
                        "{Dates} at {Times}",
                        "at {Times}",
                        "{Dates}"
                    ]
                }
            ],
            "types": [
                {
                    "name": "Selections",
                    "values": [
                        {
                            "name": {
                                "value": "Amazon Prime",
                                "synonyms": [
                                    "Amazon",
                                    "Amazon Video"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "Hulu"
                            }
                        },
                        {
                            "name": {
                                "value": "YouTube"
                            }
                        },
                        {
                            "name": {
                                "value": "Netflix"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

Code Folder structure:

- Skill Code
  - lambda
    - lambda_function.py
    - requirements.txt
    - language_strings.json
    - prompts.py
    - utils.py

lambda_function.py

## VERSION 0.8.2

# UPDATE THESE VARIABLES WITH YOUR CONFIG
HOME_ASSISTANT_URL                = 'http://alexa.mydomain.com:8123'       # REPLACE WITH THE URL FOR YOUR HA FRONTEND
VERIFY_SSL                        = False                              # SET TO FALSE IF YOU DO NOT HAVE VALID CERTS
TOKEN                             = 'eyJ0eXAiOiJK...................s2fgKv3uTlp6VXBXHsE'                                # ADD YOUR LONG LIVED TOKEN IF NEEDED OTHERWISE LEAVE BLANK

### NO NEED TO EDIT ANYTHING UNDER THE LINE ###
import logging
import urllib3
import json
import isodate
import prompts
from datetime import datetime

import ask_sdk_core.utils as ask_utils
from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.dispatch_components import AbstractExceptionHandler
from ask_sdk_core.dispatch_components import AbstractRequestInterceptor
from ask_sdk_core.handler_input import HandlerInput
from ask_sdk_model import SessionEndedReason
from ask_sdk_model.slu.entityresolution import StatusCode
from ask_sdk_model import Response


logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


INPUT_TEXT_ENTITY = "input_text.alexa_actionable_notification"

RESPONSE_YES = "ResponseYes"
RESPONSE_NO = "ResponseNo"
RESPONSE_NONE = "ResponseNone"
RESPONSE_SELECT = "ResponseSelect"
RESPONSE_NUMERIC = "ResponseNumeric"
RESPONSE_DURATION = "ResponseDuration"


class Borg:
    """Borg MonoState Class for State Persistence."""
    _shared_state = {}
    def __init__(self):
        self.__dict__ = self._shared_state

class HomeAssistant(Borg):
    """HomeAssistant Wrapper Class."""
    def __init__(self, handler_input=None):
        Borg.__init__(self)
        if handler_input:
            self.handler_input = handler_input

        self.token = self._fetch_token() if TOKEN == "" else TOKEN

        if not hasattr(self, 'ha_state') or self.ha_state is None:
            self.get_ha_state()

    def _clear_state(self):
        self.ha_state = None

    def _fetch_token(self):
        return ask_utils.get_account_linking_access_token(self.handler_input)

    def _check_response_errors(self, response):
        data = self.handler_input.attributes_manager.request_attributes["_"]
        if response.status == 401:
            logger.error("401 Error", response.data)
            speak_output = "Error 401 " + data[prompts.ERROR_401]
            return speak_output
        elif response.status == 404:
            logger.error("404 Error", response.data)
            speak_output = "Error 404 " + data[prompts.ERROR_404]
            return speak_output
        elif response.status >= 400:
            logger.error("{response.status} Error", response.data)
            speak_output = "Error {response.status} " + data[prompts.ERROR_400]
            return speak_output

        return None

    def get_ha_state(self):
        """Get State from HA."""

        http = urllib3.PoolManager(
            cert_reqs='CERT_REQUIRED' if VERIFY_SSL else 'CERT_NONE',
            timeout=urllib3.Timeout(connect=10.0, read=10.0)
        )

        response = http.request(
            'GET',
            '{}/api/states/{}'.format(HOME_ASSISTANT_URL, INPUT_TEXT_ENTITY),
            headers={
                'Authorization': 'Bearer {}'.format(self.token),
                'Content-Type': 'application/json'
            },
        )

        errors = self._check_response_errors(response)
        if not errors:
            self.ha_state = {
                "error": True,
                "text": errors
            }

        decoded_response = json.loads(response.data.decode('utf-8'))['state']

        self.ha_state = {
            "error": False,
            "event_id": json.loads(decoded_response)['event'],
            "text": json.loads(decoded_response)['text']
        }

    def post_ha_event(self, response: str, response_type: str, **kwargs):
        """Send event to HA."""

        http = urllib3.PoolManager(
            cert_reqs='CERT_REQUIRED' if VERIFY_SSL else 'CERT_NONE',
            timeout=urllib3.Timeout(connect=10.0, read=10.0)
        )

        request_body = {
            "event_id": self.ha_state['event_id'],
            "event_response": response,
            "event_response_type": response_type
        }
        request_body.update(kwargs)

        if self.handler_input.request_envelope.context.system.person:
            person_id = self.handler_input.request_envelope.context.system.person.person_id
            request_body['event_person_id'] = person_id

        response = http.request(
            'POST',
            '{}/api/events/alexa_actionable_notification'.format(HOME_ASSISTANT_URL),
            headers={
                'Authorization': 'Bearer {}'.format(self.token),
                'Content-Type': 'application/json'
            },
            body=json.dumps(request_body).encode('utf-8')
        )

        error = self._check_response_errors(response)

        if error:
            return error

        data = self.handler_input.attributes_manager.request_attributes["_"]
        speak_output = data[prompts.OKAY]
        self._clear_state()
        return speak_output

    def get_value_for_slot(self, slot_name):
        """"Get value from slot, also know as the (why does amazon make you do this)"""
        slot = ask_utils.get_slot(self.handler_input, slot_name=slot_name)
        if slot and slot.resolutions and slot.resolutions.resolutions_per_authority:
            for resolution in slot.resolutions.resolutions_per_authority:
                if resolution.status.code == StatusCode.ER_SUCCESS_MATCH:
                    for value in resolution.values:
                        if value.value and value.value.name:
                            return value.value.name


class LaunchRequestHandler(AbstractRequestHandler):
    """Handler for Skill Launch."""
    def can_handle(self, handler_input):
        return ask_utils.is_request_type("LaunchRequest")(handler_input)

    def handle(self, handler_input):
        home_assistant_object = HomeAssistant(handler_input)
        speak_output = home_assistant_object.ha_state['text']

        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask('')
                .response
        )


class YesIntentHanlder(AbstractRequestHandler):
    """Handler for Yes Intent."""
    def can_handle(self, handler_input):
        return ask_utils.is_intent_name("AMAZON.YesIntent")(handler_input)

    def handle(self, handler_input):
        home_assistant_object = HomeAssistant(handler_input)
        speak_output = home_assistant_object.post_ha_event(RESPONSE_YES, RESPONSE_YES)

        return (
            handler_input.response_builder
                .speak(speak_output)
                .response
        )


class NoIntentHanlder(AbstractRequestHandler):
    """Handler for No Intent."""
    def can_handle(self, handler_input):
        return ask_utils.is_intent_name("AMAZON.NoIntent")(handler_input)

    def handle(self, handler_input):
        home_assistant_object = HomeAssistant(handler_input)
        speak_output = home_assistant_object.post_ha_event(RESPONSE_NO, RESPONSE_NO)

        return (
            handler_input.response_builder
                .speak(speak_output)
                .response
        )


class NumericIntentHandler(AbstractRequestHandler):
    """Handler for Select Intent."""
    def can_handle(self, handler_input):
        return ask_utils.is_intent_name("Number")(handler_input)

    def handle(self, handler_input):
        home_assistant_object = HomeAssistant(handler_input)
        number  = ask_utils.get_slot_value(handler_input, "Numbers")
        if number == '?':
            raise
        speak_output = home_assistant_object.post_ha_event(number, RESPONSE_NUMERIC)

        return (
            handler_input.response_builder
                .speak(speak_output)
                .response
        )


class SelectIntentHandler(AbstractRequestHandler):
    """Handler for Select Intent."""
    def can_handle(self, handler_input):
        return ask_utils.is_intent_name("Select")(handler_input)

    def handle(self, handler_input):
        home_assistant_object = HomeAssistant(handler_input)
        selection  = home_assistant_object.get_value_for_slot("Selections")
        if selection:
            home_assistant_object.post_ha_event(selection, RESPONSE_SELECT)
            data = handler_input.attributes_manager.request_attributes["_"]
            speak_output = data[prompts.SELECTED].format(selection)
        else:
            raise

        return (
            handler_input.response_builder
                .speak(speak_output)
                .response
        )


class DurationIntentHandler(AbstractRequestHandler):
    """Handler for Select Intent."""
    def can_handle(self, handler_input):
        return ask_utils.is_intent_name("Duration")(handler_input)

    def handle(self, handler_input):
        home_assistant_object = HomeAssistant(handler_input)
        duration  = ask_utils.get_slot_value(handler_input, "Durations")
        speak_output = home_assistant_object.post_ha_event(isodate.parse_duration(duration).total_seconds(), RESPONSE_DURATION)

        return (
            handler_input.response_builder
                .speak(speak_output)
                .response
        )


class DateTimeIntentHandler(AbstractRequestHandler):
    """Handler for Select Intent."""
    def can_handle(self, handler_input):
        return ask_utils.is_intent_name("Date")(handler_input)

    def handle(self, handler_input):
        home_assistant_object = HomeAssistant(handler_input)

        dates = ask_utils.get_slot_value(handler_input, "Dates")
        times = ask_utils.get_slot_value(handler_input, "Times")

        if not dates and not times:
            raise

        data = handler_input.attributes_manager.request_attributes["_"]
        speak_output = data[prompts.ERROR_SPECIFIC_DATE]

        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask('')
                .response
        )


class CancelOrStopIntentHandler(AbstractRequestHandler):
    """Single handler for Cancel and Stop Intent."""
    def can_handle(self, handler_input):
        return (ask_utils.is_intent_name("AMAZON.CancelIntent")(handler_input) or
                ask_utils.is_intent_name("AMAZON.StopIntent")(handler_input))

    def handle(self, handler_input):
        print("CancelOrStopIntentHandler")
        data = handler_input.attributes_manager.request_attributes["_"]
        speak_output = data[prompts.STOP_MESSAGE]

        return (
            handler_input.response_builder
                .speak(speak_output)
                .response
        )


class SessionEndedRequestHandler(AbstractRequestHandler):
    """Handler for Session End."""
    def can_handle(self, handler_input):
        return ask_utils.is_request_type("SessionEndedRequest")(handler_input)

    def handle(self, handler_input):
        home_assistant_object = HomeAssistant(handler_input)
        if handler_input.request_envelope.request.reason == SessionEndedReason.EXCEEDED_MAX_REPROMPTS:
            home_assistant_object.post_ha_event(RESPONSE_NONE, RESPONSE_NONE)

        return handler_input.response_builder.response


class IntentReflectorHandler(AbstractRequestHandler):
    """The intent reflector is used for interaction model testing and debugging.
    It will simply repeat the intent the user said. You can create custom handlers
    for your intents by defining them above, then also adding them to the request
    handler chain below.
    """
    def can_handle(self, handler_input):
        return ask_utils.is_request_type("IntentRequest")(handler_input)

    def handle(self, handler_input):
        intent_name = ask_utils.get_intent_name(handler_input)
        speak_output = "You just triggered " + intent_name + "."

        return (
            handler_input.response_builder
                .speak(speak_output)
                .response
        )


class CatchAllExceptionHandler(AbstractExceptionHandler):
    """Generic error handling to capture any syntax or routing errors. If you receive an error
    stating the request handler chain is not found, you have not implemented a handler for
    the intent being invoked or included it in the skill builder below.
    """
    def can_handle(self, handler_input, exception):
        return True

    def handle(self, handler_input, exception):
        print("CatchAllExceptionHandler")
        logger.error(exception, exc_info=True)
        home_assistant_object = HomeAssistant()

        data = handler_input.attributes_manager.request_attributes["_"]
        if hasattr(home_assistant_object, 'ha_state') and home_assistant_object.ha_state != None and 'text' in home_assistant_object.ha_state:
            speak_output = data[prompts.ERROR_ACOUSTIC].format(home_assistant_object.ha_state['text'])
            return (
                handler_input.response_builder
                    .speak(speak_output)
                    .ask('')
                    .response
            )
        else:
            speak_output = data[prompts.ERROR_CONFIG].format(home_assistant_object.ha_state['text'])
            return (
                handler_input.response_builder
                    .speak(speak_output)
                    .response
            )

class LocalizationInterceptor(AbstractRequestInterceptor):
    """Add function to request attributes, that can load locale specific data."""

    def process(self, handler_input):
        locale = handler_input.request_envelope.request.locale
        logger.info("Locale is {}".format(locale[:2]))

        # localized strings stored in language_strings.json
        with open("language_strings.json") as language_prompts:
            language_data = json.load(language_prompts)
        # set default translation data to broader translation
        data = language_data[locale[:2]]
        # if a more specialized translation exists, then select it instead
        # example: "fr-CA" will pick "fr" translations first, but if "fr-CA" translation exists,
    #          then pick that instead
        if locale in language_data:
            data.update(language_data[locale])
        handler_input.attributes_manager.request_attributes["_"] = data

# The SkillBuilder object acts as the entry point for your skill, routing all request and response
# payloads to the handlers above. Make sure any new handlers or interceptors you've
# defined are included below. The order matters - they're processed top to bottom.

sb = SkillBuilder()

# register request / intent handlers
sb.add_request_handler(LaunchRequestHandler())
sb.add_request_handler(YesIntentHanlder())
sb.add_request_handler(NoIntentHanlder())
sb.add_request_handler(SelectIntentHandler())
sb.add_request_handler(NumericIntentHandler())
sb.add_request_handler(DurationIntentHandler())
sb.add_request_handler(DateTimeIntentHandler())
sb.add_request_handler(CancelOrStopIntentHandler())
sb.add_request_handler(SessionEndedRequestHandler())
sb.add_request_handler(IntentReflectorHandler())

# register exception handlers
sb.add_exception_handler(CatchAllExceptionHandler())

# register response interceptors
sb.add_global_request_interceptor(LocalizationInterceptor())

lambda_handler = sb.lambda_handler()

requirements.txt

boto3==1.9.216
ask-sdk-core==1.11.0
isodate==0.6.0

language_strings.json

{
	"en": {
	    "ERROR_401": "It looks like I am unauthorized to reach home assistant, please check your account linking or your long lived access token and try again",
		"ERROR_404": "It looks like I may not be able to find the input text entity. Please check that you've added it to home assistant and try again",
		"ERROR_400": "Could not communicate with home assistant. Please check the Amazon CloudWatch logs in the custom skill developer console.",
	    "ERROR_ACOUSTIC": "Sorry I did not catch that... <break time='200ms'/> {}",
		"ERROR_CONFIG": "Sorry, I am having trouble, please check your configuration, in the custom skill and try again.",
		"ERROR_SPECIFIC_DATE": "Sorry, I can not do specific dates right now, try a duration instead, like... in 5 hours",
		"HELP_MESSAGE": "This skill should be only reactively while triggered via home assistant.",
		"OKAY": "Okay",
		"SKILL_NAME": "home assistant custom actions",
		"STOP_MESSAGE": "Goodbye!",
		"SELECTED" : "You selected {}",
		"WELCOME_MESSAGE": "Welcome to {}. Input text is: {}"
	},
	"de": {
	    "ERROR_401": "Ich habe scheinbar keine Berechtigungen für die verbindung zu home assistant. Bitte prüfe deinen H A Benutzer oder Access Token.",
		"ERROR_404": "Ich kann kein input text Objekt in home assistant finden. Bitte überprüfe ob du dieses bereits hinzugefügt hast.",
		"ERROR_400": "Ich kann keine Verbindung zum home assistant vornehmen. Bitte prüfe die Amazon CloudWatch logdateien in der Alexa Entwickler Console.",
		"ERROR_ACOUSTIC": "Das habe ich leider nicht verstanden... <break time='200ms'/> {}",
		"ERROR_CONFIG": "Entschuldige, hier ist etwas schief gelaufen. Bitte prüfe deine Konfiugration im custom skill und versuche es erneut.",
		"ERROR_SPECIFIC_DATE": "Ich kann leider noch kein spezifisches Datum verarbeiten. Bitte nutze stattedessen Zeiträume wie beispielsweise... in 5 Stunden.",
		"HELP_MESSAGE": "Dieser Skill sollte nur reaktiv genutzt werden, wenn er via home assistant angestossen wird",
		"OKAY": "Okay",
		"SELECTED" : "Du hast {} gewählt",
		"SKILL_NAME": "home assistant custom actions",
		"STOP_MESSAGE": "Machs gut!",
		"WELCOME_MESSAGE": "Willkommen bei {}. Der input text ist: {}"
	}
}

prompts.py

# Alexa Prompts Language Constants
ERROR_401 = "ERROR_401"
ERROR_404 = "ERROR_404"
ERROR_400 = "ERROR_400"
ERROR_ACOUSTIC = "ERROR_ACOUSTIC"
ERROR_CONFIG = "ERROR_CONFIG"
ERROR_SPECIFIC_DATE = "ERROR_SPECIFIC_DATE"
HELP_MESSAGE = "HELP_MESSAGE"
OKAY = "OKAY"
SELECTED = "SELECTED"
SKILL_NAME = "SKILL_NAME"
STOP_MESSAGE = "STOP_MESSAGE"
WELCOME_MESSAGE = "WELCOME_MESSAGE"

utils.py

import logging
import os
import boto3
from botocore.exceptions import ClientError


def create_presigned_url(object_name):
    """Generate a presigned URL to share an S3 object with a capped expiration of 60 seconds

    :param object_name: string
    :return: Presigned URL as string. If error, returns None.
    """
    s3_client = boto3.client('s3',
                             region_name=os.environ.get('S3_PERSISTENCE_REGION'),
                             config=boto3.session.Config(signature_version='s3v4',s3={'addressing_style': 'path'}))
    try:
        bucket_name = os.environ.get('S3_PERSISTENCE_BUCKET')
        response = s3_client.generate_presigned_url('get_object',
                                                    Params={'Bucket': bucket_name,
                                                            'Key': object_name},
                                                    ExpiresIn=60*1)
    except ClientError as e:
        logging.error(e)
        return None

    # The response contains the presigned URL
    return response

what is the error you are receiving?

I finally just got mine to work after like 10 tries…I had to manually add the alexa media player custom component and do the cookies process.

Alexa Media Player is working perfect. Flash Briefing and Haaska part is working.

Only thing is Custom Skill give me a headache … Without Account linking it will link and is connected.
When I ask Alexa: Open Custom Action the told me there was a problem and try it later.

When I use it with Account Linking I get the error:

Error: invalid client id or redirect url.

when I want enable the skill in the Alexa App

What does your Account Linking portion of the skill look like?

When I try using Account linking this is the info in entered:

mydomain.com is placeholder. the actual URL is working to 443.
Everything is via NPM so behind NPM Home assistant is not secure.
NPM is the layer so you can acces HA via SSL.

Do you allow users to create an account or link to an existing account with you?" ENABLED
Allow users to enable skill without account linking (Recommended): DISABLED

Authorization URI: https://ha.mydomain.com/auth/authorize
Access Token URI: https://ha.mydomain.com/auth/token
Client ID: https://layla.amazon.co.uk/
Tried also:
https://pitangui.amazon.com/
https://layla.amazon.com/

Client Secret : hjkdhfjkdfhjkhjkhfjks (so its not empty)
Your Authentication Scheme: Credentials in request body

add scope: smart_home

Looks like you may have something incorrect and I would just try again; don’t ask me how I know. :thinking: happened to me.

If this does not work… I would backtrack if it has not succeeded.
Got mine going on 3rd attempt.

You didn’t have the skill named something else perhaps ???

Like “Alexa open TEST” ???

Good luck; looks like you are nearly there. :+1:

1 Like

Ok tried it again. With the TOKEN part and not Account Linking.
On the Alexa App on mobile she ask me if it worked. 3 times.

When I try it on both of my Alexa Echo’s I get the message:
Sorry I having trouble, please try it in a little while...

When I still create a automation then Alexa say:
Sorry I did not catch that... none

She say this 1 time. When I trigger automation again. there is nothing she say

Now when I try it again on my Alexa App she say:
Sorry I did not catch that... none

and everything is broken again

can anyone explain how I change Alexa’s response to MY response?

Alexa Ask: It’s getting hot in here, do you want to turn on A/C?
ResponseYes: yes
Alexa Ask: OK, to what temperature?
ResponseNumeric: 72
Alexa Says: OK, setting thermostat to 72 degrees now. (how do I get this response via Node Red?)