Jump to content

Dimming of scene and direct integration with Alexa Smart Home


capsterx

Recommended Posts

I know there are other topic on this, but I wanted to share what I had discovered.

 

1) Dimming/Brighten from the ISY console does not issue the same commands as 'Echo dim <scene>'

  The console seems to use 'LTMCON (UP)' or 'LTMCON (DOWN)' followed by 'LTMCOFF'  I assume this is the same as <begin hold dimmer>/<finish hold dimmer>

  Echo via the portal uses LTDIM 3x or LTBRITE 3x

 

2) ISY, Switchlinc and Keypad don't always agree on how much things were increased/decreased by.

    Setup A: 2 switchlinc dimmers v.37, 1 6 button Keypadlinc Dimmer v.41

    Setup B: 1 switchlinc dimmer v.37, 1 8 button keypadlinc dimmer v.37

 

  When using the console:

   Scene A:

     ISY and the 2 switches tend to agree pretty closely, the switches themselves seem to agree all the time.  The keypadlinc very much disagrees.

 

   Scene B:

     None of them really agree.  2 Brightens/Dim from console gives ~16% each time.  If you query the scene, they are quite out of sync.

 

   Now since the console uses fadeup/cancel fade semantics, I assumed it was just a timing issue. 

 

Now, this is much more reproducible then above, seems to be pretty reliable.

  When using Echo Portal (3x dim/brighten):  Off -> 1 brighten command

  Scene A:

    Isy thinks both switches and keypad at 9%.  Query all.  Both switches are at 10%.  Keypad at 9%.

 

  Scene B:  

   Isy thinks both switch and keypad at 9%.  Query both.  Switch 10%.  Keypad 19%.

 

I assume some of this may be to the version of hardware of my switchlincs or keypadlincs.  I know my keypadlincs are pretty old and dont respond to all the commands I'd like to use with them (have to have programs work around some of it)

 

Method 1:

Now for the solutions I have used.  As someone else said in another post, you can add all devices for a scene as a spoken command you wont use, then group them all together as the spoken word you will use.  Basically re-do the scene as individual devices and the group is the same as the scene.  Kinda suboptimal as the scene device list is duplicated in 2 places, but easy and quick to setup.  Though when I have had to make changes and 'forget all devices' due to structure changes, I have had to redo the groups.

 

Method 2: (the more complex method and might not be useful to anyone...but this is what I did)

This is pretty much the same as Method 1, except on/off uses a scene command.  Where as Dim/Brighten/Set use individual commands.  This was more of a 'can I make this work' project. 

 

I figured I could write a custom alexa shim to do what I wanted.  My first thought was to have a program folder like:

"MyVirtualDeviceFolder" where it had programs of dimup, dimdown, setdim, turnon, turnoff (could even have the temp settings).  

While it looks like this might be doable in v5, I couldn't find a way in v4 programming to set dim of device based on variable.  If I could set scene based on variable, this would be a great future feature for the isy portal and opens up a simple way of letting someone do whatever they wanted with scenes or devices or whatever.  I'm using 4, so I dropped this idea.

 

I wanted to be able to OAuth to the isy portal and then issue direct commands to isy, but I could not find any posts on how to do it.   If anyone knows how, let me know.  For now I used user/password auth to the ISY portal -> ISY url.

 

While I coded this to work with the isy portal, pretty sure it should work directly with ISY without a portal if your ISY is exposed to the internet.

This took me a while to get everything setup, but it my first time using any of the amazon web/lambda/etc services.  I found the amazon documentation to be very lacking and/or scattered all around.  I had to create 3 different amazon accounts and link them all in various ways just to get a test up and running that did nothing.  It took me even longer to make echo -> cloud watch log.  I'm still not sure what I did that allowed it to work.

 

Here is my best memory of the steps I used, I may of forgotten a few.  They are based on the instructions https://github.com/auchter/haaska and https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/steps-to-create-a-smart-home-skill.

 

1) Create alexa developer account.

    Log into amazon developer porta, click Alexa on top menu, get started with Skill, create new Skill.

    give it a name and choose 'smart home skill' then save and copy 'App ID'.  Ignore the rest of the configuration for now.

2) Create an AWS account, you will need a CC for this but shouldnt get charged for the amount.

    Log into AWS, select LAMBDA.  Click functions, create new function.

    search for 'smart' and select the 'alexa-smart-home-skill-adapter'  The template only exists for javascript, so dont change your language to search for.

    paste in your app ID that came from the alexa smart home skill, select enable trigger

    you may now change your code to python, which is what I did

    create a role from template and give it a name

    paste your code in (my code is at bottom of this post)

    next

    Now click 'functions' from the left hand side menu, select your function (via just the circle, if you click anywhere else it takes you to configuration)

    Goto actions and show ARN, you will need this later.

3)  This step is only because smart home skills require OAuth to somewhere, I used amazon, I'd like to use isy portal in the future.
     goto login.amazon.com and create an account
     Goto 'App Console' then 'Register a new application'
     give it a name/desc/url (i used amazon.com for privacy url).
     Goto 'Web Settings' and then edit.
     Now flip back to Alexa developer portal, on your skill, on Configuration.  Grab the URLs under 'Redirect URLs'
     You want to add these to the login settings under web settings/Allowed Return URLs
4)  Fill out rest of configuration for your skill
     Goto Configuration of your Skill in Alexa portal
     Endpoint:  Click North America (or wherever you are) and in the box enter your 'ARN' found from the lambda function.
     Authorization URL: https://www.amazon.com/ap/oa
     Client Id:  From login.amazon.com Web Settings
     Domain: I left blank
     Scope: Add one with 'profile'  (it demands at least one, this says give access to your email/name on amazon.  We wont actually use this other than it requires it)
     Access Token URI:  https://api.amazon.com/auth/o2/token
     Client Secret:  from  login.amazon.com Web Settings
     Privacy URL: some url, it requires it click save
5)  Okie dokie, you can now add the skill to your Alexa app.
     I was unable to do this from the web page, I'm not sure why, but it worked from my phone app.
     The method is the same as adding ISY portal, goto smart home skills search for whatever you named it.
     You will need to auth it to amazon then you can 'discover devices'
 
Now for the code I used.  This is very hacky, first run something I did last night.  It has a hash of 'scene name' to info.  The info describes the 'master'.  This would be the device that you are actually dimming (the switch hooked up to power).  The code uses the scene info to get the rest of the nodes and stores it in discovery.  Because of this, if you change the scene, for now, it would have to be rediscovered.  The 'config' supports naming a scene more than one thing, but I didn't finish the code that would allow that.  Each device needs a unique id, they are not generated as of now.  Most of the stuff just was copied from their example.
 
The code does the following:
  For on/off: scene on
  For dim/brighten:  Grab the status of 'master' +/- the amount, set absolute
  For set: grab status of 'master' take X percent of 255, set absolute
  It sets the absolute by setting first the master to X (0-255) then setting all the 'slave' nodes to the same value.
 
I'm sure there are much better ways, but this was fun for me to play around with.
------ snip snip----------------
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
 
import urllib, urllib2, base64
import xml.etree.ElementTree as ET
 
AUTH=('user@email.com', 'password')
SCENES = {
        '<scene name>': {
            'master': '<device that physically controls light>',
            'names': ['Name to Name it'],
            'device_info':  {
                "applianceId":"uniqueID001",
                "manufacturerName":"yourManufacturerName",
                "modelName":"model 01",
                "version":"your software version number here.",
                "friendlyDescription":"Your name Here to show up in Alexa Smart Home",
                "isReachable":True,
                "actions": ["turnOn", "turnOff", "setPercentage", "incrementPercentage", "decrementPercentage"]
                }
           }
        }
 
 
def lambda_handler(event, context):
    context.log_group_name = "/aws/lambda/handler"
 
    logger.info('got event{}'.format(event))
    access_token = event['payload']['accessToken']
 
    if event['header']['namespace'] == 'Alexa.ConnectedHome.Discovery':
        logger.info("discovery")
        return handleDiscovery(context, event)
 
    elif event['header']['namespace'] == 'Alexa.ConnectedHome.Control':
        logger.info('Control')
        return handleControl(context, event)
        
def read_page(page):
    request = urllib2.Request(PORTAL_URL + page)
    base64string = base64.encodestring('%s:%s' % (AUTH[0], AUTH[1])).replace('\n', '')
    request.add_header("Authorization", "Basic %s" % base64string)
    result = urllib2.urlopen(request)
    text = result.read()
    root = ET.fromstring(text)
    return root
 
def handleDiscovery(context, event):
    payload = ''
    header = {
        "namespace": "Alexa.ConnectedHome.Discovery",
        "name": "DiscoverAppliancesResponse",
        "payloadVersion": "2"
        }
 
    if event['header']['name'] == 'DiscoverAppliancesRequest':
        root = read_page('/nodes')
        appliances = []
        for name, settings in SCENES.iteritems():
            master_node =  root.findall("./node/[name=%s]/pnode" % settings['master'])[0].text
            nodes = [x.text for x in root.findall("./group/[name=%s]/members" % name)[0] if x.text != master_node]
            scene_address = root.findall("./group/[name=%s]/address" % name)[0].text
 
            for name in settings['names']:
                appliance = {
                  'friendlyName': name, 
                  'additionalApplianceDetails': {
                        'master_node': master_node,
                        'nodes': ','.join(nodes),
                        'scene_address': scene_address,
                    }
                }
                appliance.update(settings['device_info'])
                appliances.append(appliance)
        payload = {
            "discoveredAppliances": appliances
            }
    context.log(payload)
    return { 'header': header, 'payload': payload }
    
def setPercentage(details, amount):
    nodes = [details['master_node']]
    nodes.extend(details['nodes'].split(','))
    for node in nodes:
        page = '/nodes/%s/cmd/DON/%d' % (urllib.pathname2url(node), amount)
        logger.info("Reading page: %s" % page)
        read_page(page)
 
def handleControl(context, event):
    payload = {}
    device_id = event['payload']['appliance']['applianceId']
    message_id = event['header']['messageId']
    details = event['payload']['appliance']['additionalApplianceDetails']
 
    if event['header']['name'] == 'TurnOnRequest':
        name="TurnOnConfirmation"
        read_page('/nodes/%s/cmd/DON' % details['scene_address'])
    elif event['header']['name'] == 'TurnOffRequest':
        name="TurnOffConfirmation"
        read_page('/nodes/%s/cmd/DOF' % details['scene_address'])
    elif event['header']['name'] == 'SetPercentageRequest':
        name="SetPercentageConfirmation"
        percent=event['payload']['percentageState']['value']
        amount = int(255 * (percent / 100.0))
        setPercentage(details, amount)
    elif event['header']['name'] == 'IncrementPercentageRequest':
        name="IncrementPercentageConfirmation"
        percent=event['payload']['deltaPercentage']['value']
        data = read_page("/status/%s" % (urllib.pathname2url(details['master_node'])))
        current_amount = int(data.findall('./property')[0].attrib['value'])
        amount = min(255, current_amount + int(255 * (percent / 100.0)))
        setPercentage(details, amount)
    elif event['header']['name'] == 'DecrementPercentageRequest':
        name="DecrementPercentageConfirmation"
        percent=event['payload']['deltaPercentage']['value']
        data = read_page("/status/%s" % (urllib.pathname2url(details['master_node'])))
        current_amount = int(data.findall('./property')[0].attrib['value'])
        amount = max(0, current_amount - int(255 * (percent / 100.0)))
        setPercentage(details, amount)
    else:
        return
 
    header = {
        "namespace":"Alexa.ConnectedHome.Control",
        "name":name,
        "payloadVersion":"2",
        "messageId": message_id
        }
    return { 'header': header, 'payload': payload }
 

 

 

Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • Create New...