Giter Site home page Giter Site logo

thenewwazoo / lutron-leap-js Goto Github PK

View Code? Open in Web Editor NEW
29.0 10.0 9.0 389 KB

A TypeScript implementation of the Lutron LEAP protocol for non-pro Caseta Smart Bridge devices

JavaScript 1.83% TypeScript 98.17%
lutron lutron-caseta caseta lutron-devices lutron-leap

lutron-leap-js's Introduction

lutron-leap

This library is an implementation of Lutron's unpublished LEAP protocol. It is, in large part, a port of pylutron-caseta, without which this would not have been possible. It was written to support the homebridge-lutron-caseta-leap Homebridge plugin, but it exists independently of it and has been tested and improved in non-Caseta contexts.

Device support

Will you add support for X device? Can I?

The answer to "can this add support for a device" is probably no, but maybe not for the reason you expect.

LEAP is a protocol. It defines certain message types and objects, as well as implicitly defines some behaviors. Some of these definitions include objects that do indeed map to real-world objects, but only things that are components of the kind of devices you're asking about. As a concrete example, LEAP defines things like buttons. Not remotes, but individual buttons. So adding support for a particular remote doesn't really make sense in this library, because there's no concept of a "remote" in LEAP. There's just generic devices and button groups.

Well, can you/I add classes for those concepts?

Ehhh probably not. My preference is to keep this library "pure" as only an implementation of LEAP. I'm absolutely open to a separate, general-purpose library that contains Lutron product abstractions that use this library, but I don't have a need for one so I'm not going to write it right now.

But what about bridges? Those are devices!

That's true! Bridges are a notable exception because they are entry points into traversing the LEAP device "tree". Consumers of this library don't want or need to understand things like button groups or occupancy subscription that are specific to existing (physical) implementations of LEAP.

The "readBlindsTilt" and "setBlindsTilt" are kinda hacks that should get cleaned up in favor of an abstraction over the various CommandType values available. That should come if/when support for another bridge is added to the library.

Code structure

This code has three major components: the message parsing stack, the LEAP client, and a Caséta Smart Bridge 2 abstraction.

Message parsing

At the top level, LEAP messages have three parts:

  • A communique type, which indicates the function (subscription, information read, update, etc) and direction (response or request)
  • A header, which includes a status code, a client-supplied tag, a URL indicating the resource, and a body type
  • A body, of the specified type, containing the data being passed

Most of the code is concerned with parsing responses, as most of the job of this library is passing messages from the Lutron hub to whoever cares to listen.

Updating this code is pretty mechanical at this time, and the best way to understand it is to look at a commit that adds newly-observed objects. In short, you'll want to:

The LEAP client

The LEAP client handles reading and writing from the secure socket, as well as routing messages to subscriber callbacks. Requests are submitted with a user-supplied, arbitrary tag. This tag is returned with relevant responses. For example, if you subscribe to some event and provide tag 1, event messages will also include the 1 tag. You, dear user, don't actually care about this. All you care about is having your callback called, which the LEAP client does.

The LEAP client also passes messages that are "unsolicited", and do not have a tag. These messages are emitted by the LEAP client. Any listener can receive them and process them.

Messages that have a tag that is not recognized by the client are noisily dropped.

WARNING: The socket handling in the LEAP client class is super duper ugly. If you feel like refactoring it, please do.

Bridge-related abstractions

Because this code was written to support Caseta devices, the following section only applies to the Caseta Smart Bridge 2. I wholeheartedly welcome improvements related to other LEAP technologies.

Caseta Smart Bridge 2 and Smart Bridge 2 Pro devices are discovered by using mDNS. The BridgeFinder handles discovery and returns network information that be used by the listener to construct the actual client itself.

The SmartBridge class abstractions for relevant operations like subscribing to known device types. The goal is to relieve the user of having to know how to construct URLs and post bodies, but instead to encode that information in this library.

Real-world testing

This library has been extensively tested on Caseta Smart Bridge 2 (pro and non-pro) devices, and gracious contributors have tested it with RA3. If you have used it elsewhere, please drop a note and let me know. So far, this library has been tested with:

  • Caseta Smart Bridge 2
  • Caseta Smart Bridge 2 Pro
  • Pico remote
  • Caseta occupancy sensor
  • Serena wood blinds
  • RA3 processor
  • Sunnata Dimmer
  • Sunnata switch
  • RA3 wall motion sensor
  • RA3 ceiling mounted motion sensor

lutron-leap-js's People

Contributors

dependabot[bot] avatar maj avatar monitron avatar terafin avatar thenewwazoo avatar thibaulf avatar vpulim avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

lutron-leap-js's Issues

Support zone subscriptions

Writing this down here for docs/future:
leap.subscribe('/zone/status', function(subscribeResponse) {
logging.info('zone subscribe response: ' + subscribeResponse)
}, 'SubscribeRequest')

MultipleAreaDefinition body type

2022-01-31T00:51:04.388Z leap:message:bodytype parsing body type MultipleAreaDefinition with data: {
  Areas: [
    { href: '/area/3', Name: 'Home', SortOrder: 0, IsLeaf: false },
    {
      href: '/area/24',
      Name: 'Loft',
      Parent: { href: '/area/3' },
      SortOrder: 0,
      IsLeaf: false
    },
    {
      href: '/area/83',
      Name: 'Equipment Room',
      Parent: { href: '/area/3' },
      SortOrder: 1,
      IsLeaf: true
    },
    {
      href: '/area/729',
      Name: 'Loft',
      Parent: { href: '/area/24' },
      SortOrder: 1,
      IsLeaf: true
    }
  ]
}

Issue with mDNS Queries/Responses

I was getting the same problem as @readybeginn in homebridge-lutron-caseta-leap#10, so I poked around a bit and tried some things. I have an OPNsense router with multiple VLANs, though I've turned on the mDNS repeater and it's generally worked fine for all other things HomeKit. As far as I could tell the mDNS queries/responses were getting through the firewall, but the plugin wasn't seeing my hub at initial setup. My Homebridge also runs in Docker with --net=host.

I tried running a simple multicast-dns standalone script that fires off the same query as the plugin here and seemed to be getting responses, so I tried working through the plugin code. I found that the response packets never have an id value other than 0 for me, with the plugin or with multicast-dns independently. Not sure if that's an issue with my network/devices/firewall or what?

I also had to change the resolver configuration to use port 5353 (or leave it off). With the setting of 0 it never received any packets - I'm not sure of the total significance of using 0 here.

I edited the plugin code to instead look for incoming packet responses that had was a single-answer, was the SRV type, and had a name that matched Lutron:

const tgt: string = await new Promise((resolve, reject) => {
    resolver.on('response', (packet: any) => {
        if (packet.answers.length === 1 &&
            packet.answers[0].type === 'SRV' &&
            packet.answers[0].name.match(/[Ll]utron/)) {

This change didn't immediately work by itself, but I tried simultaneously running the independent multicast-dns script (note: running on my laptop, instead of my Homebridge server). That DID result in a response packet that matched the above criteria, and the bridge appeared in the Homebridge UI. Of course that doesn't solve the long term problem of needing to do that on every Homebridge boot so the plugin can find the Lutron bridge again.

I'm brushing the edge of my Node async experience here - it seemed like the resolver query might be going out, but before the resolver listener was running to handle it? Or some other sequencing/timing type thing? (Ignoring the whole absence of a packet id).

There's probably a much better way than this hacky approach, but I edited the getBridgeId function to the following. It uses the above packet criteria, and fires off a new query upon receiving any packet that isn't the correct one. It then destroys the resolver as (I think) it's no longer needed.

Everything seems to work fine with this code, including initial pairing and Homebridge reboots. But I totally wouldn't be surprised if there's something simple I missed!

private async getBridgeID(mdnsID: string): Promise<string> {
    const resolver = mdns({
        multicast: true,
        ttl: 1,
    });
    
    const tgt: string = await new Promise((resolve, reject) => {
        resolver.on('response', (packet: any) => {
            if (packet.answers.length === 1 &&
            packet.answers[0].type === 'SRV' &&
            packet.answers[0].name.match(/[Ll]utron/)) {
                resolve(packet.answers[0].data.target);
                // Stop the resolver now that we have our good answer
                resolver.destroy();
            } else {
                // Resend query
                resolver.query(
                    {
                        // tslint:disable:no-bitwise
                        flags: dnspacket.RECURSION_DESIRED | dnspacket.AUTHENTIC_DATA,
                        questions: [
                            {
                                name: mdnsID,
                                type: 'SRV',
                                class: 'IN',
                            },
                        ],
                        additionals: [
                            {
                                name: '.',
                                type: 'OPT',
                                udpPayloadSize: 0x1000,
                            },
                        ],
                    },
                    {
                        port: 5353,
                        address: '224.0.0.251',
                    },
                );
            }
        });
    });
    
    try {
        return tgt.match(/[Ll]utron-(?<id>\w+)\.local/)!.groups!.id.toUpperCase();
    } catch (e) {
        throw new Error(`could not get bridge serial number from ${tgt}: ${e}`);
    }
}

OneZoneDefinition body type

2022-01-31T00:48:27.601Z leap:message:bodytype parsing body type OneZoneDefinition with data: {
  Zone: {
    href: '/zone/622',
    Name: 'Loft Outdoors',
    ControlType: 'Switched',
    Category: { Type: 'OtherAmbient', IsLight: true },
    AssociatedArea: { href: '/area/729' },
    SortOrder: 0
  }
}

OneAreaDefinition body type

2022-01-31T00:51:42.604Z leap:message:bodytype parsing body type OneAreaDefinition with data: {
  Area: {
    href: '/area/729',
    Name: 'Loft',
    Parent: { href: '/area/24' },
    AssociatedZones: [ { href: '/zone/622' } ],
    AssociatedControlStations: [
      { href: '/controlstation/613' },
      { href: '/controlstation/750' }
    ]
  }
}

OneAreaStatus body type

2022-01-31T02:36:21.602Z leap:message:bodytype parsing body type OneAreaStatus with data: {
  AreaStatus: {
    href: '/area/729/status',
    Level: 100,
    OccupancyStatus: 'Occupied',
    CurrentScene: { href: '/areascene/734' }
  }
}

Note, works for sub too:
leap.subscribe('/area/status', function(subscribeResponse) { // works for subscribe!
logging.info('area subscribe response: ' + subscribeResponse)
}, 'SubscribeRequest')

Support for Sunnata 4 buttton keypad and RA2 fan via the RA3 hub?

Hi @thenewwazoo

Any plans to add support for the RA2 fan controller and Sunnata keypads? I have a couple keypads and some RA2 fan controllers coming in, if you are interested in adding support for them I could help get these added.

What would you need me to collect to determine the best way to add support for these devices? Do you need something like the log output below for the 4 button keypad? Or do you need the full log?

DEBUG:pylutron_caseta.leap:sending b'{"CommuniqueType": "ReadRequest", "Header": {"ClientTag": "0707247d-7814-4ef0-80bb-4997699866ba", "Url": "/device/1004/buttongroup/expanded"}}' DEBUG:pylutron_caseta.leap:received: {'CommuniqueType': 'ReadResponse', 'Header': {'MessageBodyType': 'MultipleButtonGroupExpandedDefinition', 'StatusCode': '200 OK', 'Url': '/device/1004/buttongroup/expanded'}, 'Body': {'ButtonGroupsExpanded': [{'href': '/buttongroup/1013', 'Parent': {'href': '/device/1004'}, 'SortOrder': 0, 'ProgrammingType': 'Freeform', 'Buttons': [{'href': '/button/1014', 'ButtonNumber': 1, 'ProgrammingModel': {'href': '/programmingmodel/1015', 'ProgrammingModelType': 'AdvancedToggleProgrammingModel'}, 'Parent': {'href': '/buttongroup/1013'}, 'Name': 'Button 1', 'Engraving': {'Text': ''}, 'AssociatedLED': {'href': '/led/1009'}}, {'href': '/button/1018', 'ButtonNumber': 2, 'ProgrammingModel': {'href': '/programmingmodel/1019', 'ProgrammingModelType': 'AdvancedToggleProgrammingModel'}, 'Parent': {'href': '/buttongroup/1013'}, 'Name': 'Button 2', 'Engraving': {'Text': ''}, 'AssociatedLED': {'href': '/led/1010'}}, {'href': '/button/1022', 'ButtonNumber': 3, 'ProgrammingModel': {'href': '/programmingmodel/1023', 'ProgrammingModelType': 'AdvancedToggleProgrammingModel'}, 'Parent': {'href': '/buttongroup/1013'}, 'Name': 'Button 3', 'Engraving': {'Text': ''}, 'AssociatedLED': {'href': '/led/1011'}}, {'href': '/button/1026', 'ButtonNumber': 4, 'ProgrammingModel': {'href': '/programmingmodel/1027', 'ProgrammingModelType': 'AdvancedToggleProgrammingModel'}, 'Parent': {'href': '/buttongroup/1013'}, 'Name': 'Button 4', 'Engraving': {'Text': ''}, 'AssociatedLED': {'href': '/led/1012'}}]}]}} DEBUG:pylutron_caseta.leap:sending b'{"CommuniqueType": "ReadRequest", "Header": {"ClientTag": "71ca268a-0d0a-4d67-b483-40963292167a", "Url": "/device/1004"}}' DEBUG:pylutron_caseta.leap:received: {'CommuniqueType': 'ReadResponse', 'Header': {'MessageBodyType': 'OneDeviceDefinition', 'StatusCode': '200 OK', 'Url': '/device/1004'}, 'Body': {'Device': {'href': '/device/1004', 'Name': 'Device 3', 'Parent': {'href': '/project'}, 'SerialNumber': 76775880, 'ModelNumber': 'RRST-W4B-XX', 'DeviceType': 'SunnataKeypad', 'AssociatedArea': {'href': '/area/520'}, 'LinkNodes': [{'href': '/device/1004/linknode/1006'}], 'FirmwareImage': {'href': '/firmwareimage/1004'}, 'DeviceClass': {'HexadecimalEncoding': '1270101'}, 'AddressedState': 'Addressed'}}} DEBUG:pylutron_caseta.leap:sending b'{"CommuniqueType": "ReadRequest", "Header": {"ClientTag": "a13c4c52-fbd9-48c1-84ca-63dc9ed9bd52", "Url": "/area/520/associatedzone"}}'

OneProjectDefinition body type

2022-01-31T01:21:23.415Z leap:message:bodytype parsing body type OneProjectDefinition with data: {
  Project: {
    href: '/project',
    Name: 'Home',
    ProductType: 'Lutron RadioRA 3 Project',
    MasterDeviceList: { Devices: [ { href: '/device/96' } ] },
    Contacts: [ { href: '/contactinfo/81' } ],
    TimeclockEventRules: { href: '/project/timeclockeventrules' },
    ProjectModifiedTimestamp: {
      Year: 2022,
      Month: 1,
      Day: 28,
      Hour: 19,
      Minute: 24,
      Second: 29,
      Utc: '0'
    }
  }
}

Homeworks QSX support

I see that pylutron_caseta has added support specifically for Homeworks QSX. Is that something that can be added to this library. I think there are nuances between RA3 and QSX if I'm not mistaken.

OnePresetDefinition body type

2022-01-31T02:43:34.601Z leap:message:bodytype parsing body type OnePresetDefinition with data: { Preset: { href: '/preset/734', Parent: { href: '/areascene/734' } } }

Control devices?

First off, thank you for the awesome library! Exactly what I was looking for!

One thing that I didn't see in here is an ability to control lights/dimmers?

I checked out the sister project, https://github.com/thenewwazoo/homebridge-lutron-caseta-leap,
and it mentions that light/dimmer support is already inside of HomeKit so it's not needed in this library?
Unfortunately, I'm not using HomeKit, so I'm going to have to create that from scratch.

If you could point me in right direction to add output support, I would be very appreciative!

Question: Where to get information about LEAP protocol?

@thenewwazoo I am trying to implement support for Lutron Radio RA3 in .NET/C# and for that, I am trying to understand LEAP Protocol. In the README of this project, you mentioned that the LEAP protocol is unpublished. I would like to know how you got all the information about the LEAP protocol to write this implementation.

Although I can inspect the code to know the workings of the LEAP protocol and its commands, it will be good to have a document that I can refer to whenever needed. Is there any such document? Or inspecting the code is the only resort? I am not able to find much reference about LEAP protocol on Lutron's Website.

I am specifically interested in how to obtain the Global Certificate (i.e. CA Cert, Cert, and Key placed in the Association.ts file) used for pairing with the Lutron Radio RA3. Although I can directly use them, I would like to know how they are generated to get a better understanding of the workings of the LEAP protocol.

P.S.: I am posting this question as an Issue since there is no dedicated Discussions tab for this repository.

OneLinkNodeDefinition body type

2022-01-31T01:48:55.288Z leap:message:bodytype parsing body type OneLinkNodeDefinition with data: {
  LinkNode: {
    href: '/device/615/linknode/617',
    Parent: { href: '/device/615' },
    LinkType: 'ClearConnectTypeX',
    RFProperties: {},
    AssociatedLink: { href: '/link/98' }
  }
}

MultipleZoneStatus body type

2022-01-31T02:02:56.222Z leap:message:bodytype parsing body type MultipleZoneStatus with data: {
  ZoneStatuses: [
    {
      href: '/zone/622/status',
      Level: 0,
      SwitchedLevel: 'Off',
      Zone: { href: '/zone/622' },
      StatusAccuracy: 'Good'
    }
  ]
}

OneControlStationDefinition body type

2022-01-31T00:55:30.870Z leap:message:bodytype parsing body type OneControlStationDefinition with data: {
  ControlStation: {
    href: '/controlstation/613',
    Name: 'Loft Outdoor Lights',
    AssociatedArea: { href: '/area/729' },
    SortOrder: 0,
    AssociatedGangedDevices: [
      {
        Device: {
          href: '/device/615',
          DeviceType: 'SunnataSwitch',
          AddressedState: 'Addressed'
        },
        GangPosition: 0
      }
    ]
  }
}

MultipleAreaStatus body type

2022-01-31T02:40:43.349Z leap:message:bodytype parsing body type MultipleAreaStatus with data: {
  AreaStatuses: [
    {
      href: '/area/3/status',
      OccupancyStatus: 'Unknown',
      CurrentScene: null
    },
    {
      href: '/area/24/status',
      OccupancyStatus: 'Unknown',
      CurrentScene: null
    },
    {
      href: '/area/83/status',
      OccupancyStatus: 'Unknown',
      CurrentScene: null
    },
    {
      href: '/area/729/status',
      Level: 100,
      OccupancyStatus: 'Occupied',
      CurrentScene: { href: '/areascene/734' }
    }
  ]
}

MultipleLinkDefinition body type

2022-01-31T02:23:31.436Z leap:message:bodytype parsing body type MultipleLinkDefinition with data: {
  Links: [
    { href: '/link/100', Parent: { href: '/system' }, LinkType: 'RF' },
    {
      href: '/link/98',
      Parent: { href: '/system' },
      LinkType: 'ClearConnectTypeX',
      ClearConnectTypeXLinkProperties: {
        PANID: XXXXX,
        ExtendedPANID: 'XXXXX',
        Channel: 25,
        NetworkName: '',
        NetworkMasterKey: 'XXXXX'
      }
    }
  ]
}

OneAreaSceneDefinition body type

2022-01-31T02:42:53.021Z leap:message:bodytype parsing body type OneAreaSceneDefinition with data: {
  AreaScene: {
    href: '/areascene/734',
    Name: 'Scene 001',
    Parent: { href: '/area/729' },
    Preset: { href: '/preset/734' },
    SortOrder: 1
  }
}

Support new Homeworks QSX LEAP varient

There is a new (I think beta) way of using the leap protocol. I have a rudimentary implementation in my https://github.com/JJTech0130/llfp repository, but I would like to integrate it into this one.
Instead of requiring certs, you need to send a log in packet with a username/password.
You use with a TLS socket on port 8081.
I will try and implement here when I get some time.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.