staromeste / homebridge-http-advanced-accessory

Supports all devices on HomeBridge Platform / Bridges devices to http
Apache License 2.0
65 stars 22 forks source link

homebridge http advanced accessory

Homebridge plugin that can turn virtually any device which exposes HTTP APIs into an HomeKit-compatible Service. Its purpose is to connect any device that can be controlled via HTTP command to Homekit. It creates a Homebridge accessory which uses HTTP calls to change and check its state via Actions.

This plugin is a fork of HttpAccessory and has merged many features (mainly mappers) from the homebridge-http-securitysystem.

Installation

  1. Install homebridge using: npm install -g homebridge
  2. Install this plugin using: npm install -g homebridge-http-advanced-accessory
  3. Update your configuration file. See sample-config.json in this repository for a sample.

Features

The main function of the module is to proxy HomeKit queries to an arbitrary web API to retrieve and set the status of the accessory. Main features include:

Configuration

Configuration sample:

 {
    "bridge": {
        "name": "Homebridge",
        "username": "C1:38:5A:AC:39:30",
        "port": 51826,
        "pin": "123-45-678"
    },
    "description": "This is an example configuration for the Everything Homebridge plugin",
    "accessories": [
        {
            "accessory": "HttpAdvancedAccessory",
            "service": "ContactSensor",
            "name": "Terrace Sensor",
            "forceRefreshDelay": 5,
            "username": "admin",
            "password": "admin",
            "debug" : false,
            "optionCharacteristic" :[],
            "urls":{
               "getContactSensorState": {
                  "url" : "http://remoteserver/xml/zones/zonesStatus48IP.xml",
                  "mappers" : [
                      {
                          "type": "xpath",
                          "parameters": {
                              "xpath": "//status[1]/text()"
                          }
                      },
                      {
                          "type": "static",
                          "parameters": {
                              "mapping": {
                                  "ALARM": "1",
                                  "NORMAL":"0"
                              }
                          }
                     }
                  ]
               }
            }
         },
         {
         "accessory": "HttpAdvancedAccessory",
         "service": "SecuritySystem",
         "name": "Btcino Security",
         "forceRefreshDelay": 5,
         "username": "admin",
         "password": "admin",
         "debug" : false,
         "setterDelay" : 1000,
         "urls":{
            "getSecuritySystemTargetState": {
               "url" : "http://remoteserver/xml/state/virtualKeypad.xml",
               "mappers" : [
                   { "type": "xpath",  "parameters": { "xpath": "//generic/text()" } },
                   { "type": "static", "parameters": { "mapping": { "0": "3", "1": "1", "2": "2", "3": "0" } } }
               ]
            },
            "getSecuritySystemCurrentState": {
               "url" : "http://remoteserver/xml/partitions/partitionsStatus48IP.xml", 
               "mappers" : [
                  { "type": "regex",  "parameters": { "regexp" : "(ALARM)",    "capture": "1" } },
                  { "type": "regex",  "parameters": { "regexp" : ">(ARMED)",   "capture": "1" } },
                  { "type": "regex",  "parameters": { "regexp" : "(DISARMED)", "capture": "1" } },
                  { "type": "static", "parameters": { "mapping": { "ALARM": "4", "ARMED":"inconclusive", "DISARMED": "3"} } }
               ],
               "inconclusive" : {
                  "url" : "http://remoteserver/xml/state/virtualKeypad.xml", 
                  "mappers" : [
                      { "type": "xpath",  "parameters": { "xpath": "//generic/text()" } },
                      { "type": "static", "parameters": { "mapping": { "0": "3", "1": "1", "2": "2", "3": "0" } } }
                  ]
               }
            },
            "setSecuritySystemTargetState": {
               "url" : "http://remoteserver/xml/cmd/cmdOk.xml?cmd=setMacro&macroId={value}&redirectPage=/xml/cmd/cmdError.xml",
               "mappers" : [
                   { "type": "static", "parameters": { "mapping": { "0": "3", "1": "1", "2": "2", "3": "0" } } }
               ]
            }
         }
         }
    ],

    "platforms": []
}

Actions

The action is a key-value map that configures the URLs to be called to perform a read or a write on a particular Characteristic. In fact, there are two kind of actions, getters and setters: actions for getter keys begin with word "get", actions for the setters begin with "set". So the key name is composed of two parts:

For example, to get the value of the SecuritySystemTargetState Characteristic, the key value would be "getSecuritySystemTargetState"—while to set it, "setSecuritySystemTargetState"

Getter Action

The value object has the following JSON format for a getter action:

"getTargetTemperature" : {
    "url":"http://",
    "httpMethod":"",
    "body" : "",
    "mappers" : [],
    "inconclusive" : {
        "url":"",
        "httpMethods":"",
        "mappers": [],
        "inconclusive":{}
    }
}

Where:

Setter Action

The value object has the following JSON format for a setter action:

"setTargetTemperature" : {
    "url":"http://remoteserver/setTemperature?stemp={value}",
    "httpMethod":"",
    "body" : "{value}",
    "mappers" : []
}

Where:

URL Template

The URL can be a string template so you can use expressions like $(state.getCurrentTemperature) that will be replaced by the current value of the Characteristic CurrentTemperature. The state variable contains all the values of the Characteristics of the Service, plus the value variable contains the value HK wants to set for the Characteristic being set. For example, suppose that when setting the Active state of a HeatingCooling system it also needs to set the TargetTemperature in Fahrenheit—you may have something like this:

"setActive" : {
    "url":"http://remoteserver/setActive?${value}&stemp=${state.getTargetTemperature * 9/5 +32}"
}

Mapping

The mappings block of the configuration may contain any number of mapper definitions. The mappers are chained after each other—the result of a mapper is fed into the input of the next mapper. The purpose of this whole chain is to somehow boil down the response received from the API to a single value which is expected by HomeKit.

Each mapper has the following JSON format:

{
    "type": "<type of the mapper>",
    "parameters": { <parameters to be passed to the mapper> }
}

There are 3 kinds of mappers implemented at the moment.

Static mapper

The static mapper can be used to define a key => value dictionary. It will simply look up the input in the dictionary and if it is found, it returns the corresponding value. It's great for mapping string responses like "ARMED" to their actual number.

Configuration is as follows:

{
    "type": "static",
    "parameters": { 
        "mapping": {
            "STAY": "0",
            "AWAY": "1",
            "whataever you don't like": "whatever you like more"
        }
    }
}

This configuration would map STAY to 0, AWAY to 1 and "whatever you don't like" to "whatever you like more". If the mapping does not have an entry which corresponds to input, it returns the full input.

Regexp mapper

The regexp mapper can be used to define a regular expression to run on the input, capture some substring of it and return it. It's great for mapping string responses which may change around but have a certain part that's always there and which is the part you are interested in.

Configuration is as follows:

{
    "type": "regex",
    "parameters": {
        "regexp": "^The system is currently (ARMED|DISARMED), yo!$",
        "capture": "1"
    }
}

This configuration will run the regular expression defined by the regexp parameter against the input and return the first capture group (as defined by capture). So, in this case, if the input is "The system is currenty ARMED, yo!", the mapper will map this to "ARMED".

If the regexp does not match the input, the mapper returns the full input.

XPath mapper

The XPath mapper can be used to extract data from an XML document. It allows the definition of an XPath which will then be applied to the input and returns whatever the query selects.

When using this mapper, make sure that you select text elements and not entire nodes or node lists, otherwise it will fail horribly.

Configuration is as follows:

{
    "type": "xpath",
    "parameters": {
        "xpath": "//partition[3]/text()",
        "index": 0
    }
}

Let's assume this mapper gets the following input:

<?xml version="1.0" encoding="ISO-8859-1"?>
<partitionsStatus>
    <partition>ARMED</partition>
    <partition>ARMED</partition>
    <partition>ARMED_IMMEDIATE</partition>
</partitionsStatus>

In this case this mapper will return "ARMED_IMMEDIATE". The index parameter can be used to specify which element to return if the xpath selects multiple elements. In the example above it is completely redundant as partition[3] already makes sure that a single partition is selected.

JSONPath mapper

The JSONPath mapper can be used to extract data from a JSON object. See https://www.npmjs.com/package/JSONPath#syntax-through-examples for syntax and more examples.

When using this mapper, make sure that you select text elements or arrays and not entire objects.

Configuration is as follows:

{
    "type": "jpath",
    "parameters": {
        "jpath": "$.partitionsStatus.partition[2]",
        "index": 0
    }
}

Let's assume this mapper gets the following input:

{
    "partitionsStatus": {
        "partition": [
            "ARMED",
            "ARMED",
            "ARMED_IMMEDIATE",
        ]
    }
}

In this case this mapper will return "ARMED_IMMEDIATE". The index parameter can be used to specify which element to return if the JSONPath selects multiple elements. In the example above it is completely redundant as partition[2] already makes sure that a single partition is selected.

Eval mapper

The eval mapper can be used to run any JavaScript code, which will be interpreted when the event is called. Use value to use the set/read value in your code.

Configuration is as follows:

{
    "type": "eval",
    "parameters": {
        "expression": "value === \"OK\" ? 1 : 0"
    }
}

In this example, if the mapper receives the string OK it will return 1, for anything else it will return 0.
Be careful with the code you write, this mapper can be very flexible but it can also blow up quite easily.

Supported services

AccessoryInformation AirQualitySensor BatteryService BridgeConfiguration BridgingState CameraControl CameraRTPStreamManagement CarbonDioxideSensor CarbonMonoxideSensor ContactSensor Door Doorbell Fan GarageDoorOpener HumiditySensor LeakSensor LightSensor Lightbulb LockManagement LockMechanism Microphone MotionSensor OccupancySensor Outlet Pairing ProtocolInformation Relay SecuritySystem SmokeSensor Speaker StatefulProgrammableSwitch StatelessProgrammableSwitch Switch TemperatureSensor Thermostat TimeInformation TunneledBTLEAccessoryService Window WindowCovering

Configuration Examples

The purpose of this section is collect as many configuration examples as possible.

Bticino "Nuovo antifurto filare"

This first example is to configure a Bticino (BT-4200, 4201, 4202) as a HomeKit SecuritySystem

    {
        "accessory": "HttpAdvancedAccessory",
        "service": "SecuritySystem",
        "name": "Btcino Security",
        "forceRefreshDelay": 5,
        "username": "admin",
        "password": "admin",
        "debug" : false,
        "urls":{
            "getSecuritySystemTargetState": {
                "url" : "http://remoteserver/xml/state/virtualKeypad.xml",
                "mappers" : [
                    { "type": "xpath",  "parameters": { "xpath": "//generic/text()" } },
                    { "type": "static", "parameters": { "mapping": { "0": "3", "1": "1", "2": "2", "3": "0" } } }
                ]
            },
            "getSecuritySystemCurrentState": {
                "url" : "http://remoteserver/xml/partitions/partitionsStatus48IP.xml",
                "mappers" : [
                    { "type": "regex",  "parameters": { "regexp" : "(ALARM)",    "capture": "1" } },
                    { "type": "regex",  "parameters": { "regexp" : ">(ARMED)",   "capture": "1" } },
                    { "type": "regex",  "parameters": { "regexp" : "(DISARMED)", "capture": "1" } },
                    { "type": "static", "parameters": { "mapping": { "ALARM": "4", "ARMED":"inconclusive", "DISARMED": "3"} } }
                ],
                "inconclusive" : {
                    "url" : "http://remoteserver/xml/state/virtualKeypad.xml", 
                    "mappers" : [
                        { "type": "xpath",  "parameters": { "xpath": "//generic/text()" } },
                        { "type": "static", "parameters": { "mapping": { "0": "3", "1": "1", "2": "2", "3": "0" } } }
                    ]
                }
            },
            "setSecuritySystemTargetState": {
                "url" : "http://remoteserver/xml/cmd/cmdOk.xml?cmd=setMacro&macroId={value}&redirectPage=/xml/cmd/cmdError.xml",
                "mappers" : [
                    { "type": "static", "parameters": { "mapping": { "0": "3", "1": "1", "2": "2", "3": "0" } } }
                ]
            }
        }
    }

Bticino "Nuovo antifurto filare" Zones as ContactSensor

{
    "accessory": "HttpAdvancedAccessory",
    "service": "ContactSensor",
    "name": "Terrace Sensor",
    "forceRefreshDelay": 5,
    "username": "admin",
    "password": "admin",
    "debug" : false,
    "urls":{
        "getContactSensorState": {
            "url" : "http://remoteserver/xml/zones/zonesStatus48IP.xml",
            "mappers" : [
                {
                    "type": "xpath",
                    "parameters": {
                        "xpath": "//status[1]/text()"
                    }
                },
                {
                    "type": "static",
                    "parameters": {
                        "mapping": {
                            "ALARM": "1",
                            "NORMAL":"0"
                        }
                    }
                }
            ]
        }
    }
}

Daikin as HeaterCooler

This is still incomplete but the unofficial Daikin documentation can help you to complete it.

{
    "accessory": "HttpAdvancedAccessory",
    "service": "HeaterCooler",
    "name": "Condizionatore Soggiorno",
    "forceRefreshDelay": 5,
    "debug" : false,
    "urls":{
        "getCurrentHeaterCoolerState": {
            "url" : "http://192.168.x.x/aircon/get_control_info",
            "mappers" : [
                {"type": "regex", "parameters": {"regexp": "(pow=0)","capture": "1"} },
                {"type": "regex", "parameters": { "regexp": "mode=(\\d)", "capture": "1"} },
                {"type": "static", "parameters": { "mapping": { "pow=0": "0", "3":"3", "4":"2"} } }
            ]
        },
        "getTargetHeaterCoolerState":{
            "url" : "http://192.168.x.x/aircon/get_control_info", 
            "mappers" : [
                {"type": "regex", "parameters": {"regexp": "(pow=0)","capture": "1"} },
                {"type": "regex", "parameters": { "regexp": "mode=(\\d)", "capture": "1"} },
                {"type": "static", "parameters": { "mapping": { "pow=0": "0", "3":"3", "4":"2", "0":"3", "1":"3", "7":"3", "2":"3"} } }
            ]
        },
        "setTargetHeaterCoolerState":{
            "url" : "http://192.168.x.x/aircon/set_control_info/{value}",
            "mappers" : [
                {"type": "static", "parameters": { "mapping": { "0": "?mode=0", "1":"?mode=4", "2":"?mode=3"} } }
            ]
        },
        "getActive":{
            "url" : "http://192.168.x.x/aircon/get_control_info", 
            "mappers" : [
                {"type": "regex", "parameters": {"regexp": "pow=(\\d)","capture": "1"} }
            ]
        },
        "setActive":{
            "url" : "http://192.168.x.x/aircon/set_control_info/{value}",
            "mappers" : [
                {"type": "static", "parameters": { "mapping": { "0": "?pow=0", "1":"?pow=1"} } }
            ]
        }

    }
}

Yamaha Musiccast WX-010 as Switch

{
    "accessory": "HttpAdvancedAccessory",
    "service": "Switch",
    "name": "Bedroom speaker",
    "forceRefreshDelay": 5,
    "debug" : false,
    "urls":{
        "getOn":{
            "url" : "http://192.168.x.x/YamahaExtendedControl/v1/main/getStatus",
            "mappers" : [
                {
                    "type": "jpath",
                    "parameters": {
                        "jpath": "$..power",
                        "index": "0"
                    }
                },
                {
                    "type": "static",
                    "parameters": {
                        "mapping": {
                            "on": "1",
                            "standby": "0"
                        }
                    }
                }
            ]
        },
        "setOn":{
            "url" : "http://192.168.x.x/YamahaExtendedControl/v1/main/setPower?power=${value==1?\"on\":\"standby\"}"
        }

    }
}

Generic Web API as Lightbulb

{
    "accessory": "HttpAdvancedAccessory",
    "service": "Lightbulb",
    "name": "Pool Light",
    "manufacturer": "Custom",
    "model": "Virtual Device",
    "debug": false,
    "optionCharacteristic": [
        "Hue",
        "Saturation",
        "Brightness"
    ],
    "urls": {
        "setOn": {
            "httpMethod": "POST",
            "body": "%7B%22c%22%3A%22pool%20i%20{value}%22%7D",
            "url": "http://127.0.0.1/control.php",
            "mappers": [
                {
                    "type": "static",
                    "parameters": {
                        "mapping": {
                            "true": "on",
                            "false": "off"
                        }
                    }
                }
            ]
        },
        "getOn": {
            "httpMethod": "POST",
            "body": "%7B%22c%22%3A%22update%20pool-on%22%7D",
            "url": "http://127.0.0.1/control.php",
            "mappers": [
                {
                    "type": "jpath",
                    "parameters": {
                        "jpath": "$.u",
                        "index": 0
                    }
                }
            ]
        },
        "setHue": {
            "httpMethod": "POST",
            "url": "http://127.0.0.1/control.php",
            "body": "%7B%22c%22%3A%22pool%20hue%20{value}%22%7D",
            "mappers": []
        },
        "getHue": {
            "httpMethod": "POST",
            "url": "http://127.0.0.1/control.php",
            "body": "%7B%22c%22%3A%22value%20pool-h%22%7D",
            "mappers": [
                {
                    "type": "jpath",
                    "parameters": {
                        "jpath": "$.u",
                        "index": 0
                    }
                }
            ]
        },
        "setSaturation": {
            "httpMethod": "POST",
            "url": "http://127.0.0.1/control.php",
            "body": "%7B%22c%22%3A%22pool%20saturation%20{value}%22%7D",
            "mappers": []
        },
        "getSaturation": {
            "httpMethod": "POST",
            "url": "http://127.0.0.1/control.php",
            "body": "%7B%22c%22%3A%22value%20pool-s%22%7D",
            "mappers": [
                {
                    "type": "jpath",
                    "parameters": {
                        "jpath": "$.u",
                        "index": 0
                    }
                }
            ]
        },
        "setBrightness": {
            "httpMethod": "POST",
            "url": "http://127.0.0.1/control.php",
            "body": "%7B%22c%22%3A%22pool%20brightness%20{value}%22%7D",
            "mappers": []
        },
        "getBrightness": {
            "httpMethod": "POST",
            "url": "http://127.0.0.1/control.php",
            "body": "%7B%22c%22%3A%22value%20pool-b%22%7D",
            "mappers": [
                {
                    "type": "jpath",
                    "parameters": {
                        "jpath": "$.u",
                        "index": 0
                    }
                }
            ]
        }
    }
}

Plugin Development

To aid in testing and developing this plugin further I have provided a sample homebridge config. This will allow you to spin a homebridge instance for development that has this plugin already installed.
Install homebridge, checkout this repo and run:

$ homebridge --debug --user-storage-path .homebridge-dev --plugin-path ./