6. Other IP based devices - light
Hardware Setup
Philip Hue is a system of 4 components:
- Apps – These are ways to control the lights to make them do smart things.
- Bridge – This is used to enable your smart bulbs to communicate with each other and the Portal via the internet. The main set of APIs are those offered by the bridge. These allow you to control all settings of the lights in your system. These APIs require direct access to your bridge so you’ll only be able to access them when your app and bridge are on the same local network.
- Portal – This is a web based control panel which connects your home to the internet. It delivers control commands from outside and keeps your software in the bridge up-to-date. The portal presents a utility API to help you discover the address of your bridge.
- Lights – This is the output of the system. These smart bulbs contain 3 types of LED specifically chosen to produce a range of colors and intensities. Lights create a mesh network with each other which enables each light to pass on messages to the next extending the range and making everything more robust. They are connected to the bridge via an open standards protocol called ZigBee Light Link.
We will be controlling the lights using Philip Bridge HTTP APIs.
Philip Bridge HTTP Requests
http://10.0.2.16/api/newdeveloper/lights
Request Name: Get the list of lights
{ "1": { "state": { "on": true, "bri": 144, "hue": 13088, "sat": 212, "effect": "none", "xy": [ 0.5128, 0.4147 ], "ct": 467, "alert": "none", "colormode": "hs", "reachable": true }, "type": "Extended color light", "name": "Hue Lamp 1", "modelid": "LCT001", "uniqueid": "00:17:88:01:00:b6:c6:f0-0b", "swversion": "66009663", "pointsymbol": { "1": "none", "2": "none", "3": "none", "4": "none", "5": "none", "6": "none", "7": "none", "8": "none" } }, "2": { "state": { "on": true, "bri": 144, "hue": 13088, "sat": 212, "effect": "none", "xy": [ 0.5128, 0.4147 ], "ct": 467, "alert": "none", "colormode": "ct", "reachable": true }, "type": "Extended color light", "name": "Hue Lamp 2", "modelid": "LCT001", "uniqueid": "00:17:88:01:00:b9:38:7c-0b", "swversion": "66009663", "pointsymbol": { "1": "none", "2": "none", "3": "none", "4": "none", "5": "none", "6": "none", "7": "none", "8": "none" } }, "3": { "state": { "on": true, "bri": 144, "hue": 13088, "sat": 212, "effect": "none", "xy": [ 0.5128, 0.4147 ], "ct": 467, "alert": "none", "colormode": "hs", "reachable": true }, "type": "Extended color light", "name": "Hue Lamp 3", "modelid": "LCT001", "uniqueid": "00:17:88:01:00:b9:38:5c-0b", "swversion": "66009663", "pointsymbol": { "1": "none", "2": "none", "3": "none", "4": "none", "5": "none", "6": "none", "7": "none", "8": "none" } } }
Request Name: Change the state of Light
http://10.0.2.16/api/newdeveloper/lights/1/state
{"on":true}
Write a ThingFacet, Workflow and Device Logic (Message Transformation)
Write a ThingFacet
To abstract the interactions with the Philip Light Bulbs, we create a PhilipLightFacet ThingFacet.
We add two types of attributes in this ThingFacet
- Parameters required to make a protocol specific invocation
Attributes to store the information received from the light.
There are two important patterns to look for:
Mapping of Device Vendor Complex Structure: The response from GetLights is a complex hierarchical response.
Helper Tags: Tags that are helpful in simplifying long name as well as parameterizing the Protocol Parameters instead of attributes of a ThingFacet
Let's create Types (Def) to match to the response structure of Lights REST API
<Def Name="PhilipLightState"> <Boolean Name="On"/> <Integer Name="bri"/> <Integer Name="hue"/> <Integer Name="sat"/> <String Name="effect"/> <Double Name="xy" cardinality="2"/> <Integer Name="ct"/> <String Name="alert"/> <String Name="colormode"/> <Boolean Name="reachable"/> </Def>
Helper Tag - 1: Simply define the Philip Light Bridge API URL using PhilipLightBaseURI Tag
<PhilipLightBaseURI>http://10.0.2.16/api/newdeveloper</PhilipLightBaseURI>
Lets now create a ThingFacet using the PhilipLightState type and rest of the parameters like - Type, modelId, etc as parameters of the PhilipLightFacet ThingFacet. Now the entire ThingFacet definition maps to the exact Response of the GetLight structure.
Helper Tag - 2: Tag AA to designate Event.Argument
<ThingFacet Name="PhilipLightFacet"> <String Name="LightNumber"/> <PhilipLightState Name="State"/> <String Name="Type"/> <String Name="modelid"/> <String Name="uniqueid"/> <String Name="swversion"/> <!-- Helper Tag --> <AA>[:#o#Event.Argument:]</AA> </ThingFacet>
Write a Action with Workflow
The next step in ThingFacet is writing a Action which contains a workflow responsible for making external protocol specific call and store the output in the appropriate ThingFacet attribute. An important decision while writing Action is correct protocol selection. See the /wiki/spaces/TQLDocs/pages/1179871 in the Working with Things section. For this Philip Light we need simple HTTP request, which is part of the TQLEngine.
1. Write an Action name as PhilipLightAction
<ThingFacet Name="PhilipLightFacet"> ... <Action Name="PhilipLightAction"> ... </Action> </ThingFacet>
2. Create a Workflow within the Action, with one single continuous running Task and waiting for the Event with its ActionArgument.
<Action Name="PhilipLightAction"> <Workflow Limit="1" Live="1" Timeout="-1"> <Task name="Main" while="true"> <Event name="Argument" as="ActionArgument"/> ... </Task> </Workflow> </Action>
Here we used three modifiers for this workflow. Limit = "1" means there can be at most one instance of this workflow waiting. Live = "1" means there can be at most one instance of this workflow running. Timeout ="-1" means this workflow will never be timed out. We used a modifier while = "true" with the Task to make the workflow running in a continuous loop, because it needs to run repeatedly, not just once. For more details, refer to workflow modifiers and the lifecycle of a workflow.
The task will be activated by the event handler Event called ActionArgument. "ActionArgument" is the event generated whenever the attribute(s) associated with this Action is modified (See Associate Action with a ThingFacet Attribute). ActionArgument carries all the current values of the ThingFacet attributes, which can be used in the task if needed.
3. Invoke HTTP call: Method of HTTP is PUT. We use Invoke modifier Put for this purpose. The parameters required for HTTP PUT are passed as part of <Message> and <Value>.
Note that: xmlns="something" is added to disable tag normalization. Tag normalization is performed any time an XML (or JSON) text is converted into a ListMap structure. To prevent normalization you can specify a non-default XML namespace on the element you want normalization to be disabled. Example: <Message xmlns="PLight" to disable normalization for the whole query).
<Invoke Name="SetState" waitFor="ActionArgument" Put="[:PhilipLightBaseURI:]/lights/[%:[:AA:].LightNumber.Value:%]/state"> <Message xmlns="PLight" Type="json"> <on_true> <on>true</on> </on_true> <on_false> <on>false</on> </on_false> <Value> <Include>[:@WFRT:]on_[%:[:AA:].state.On.Value:%]</Include> </Value> </Message> </Invoke>
Note that PhilipLightBaseURI is referenced using TP tag: [:PhilipLightBaseURI:] The notation [%: is part of the /wiki/spaces/TEACH/pages/21170773. More details on /wiki/spaces/TEACH/pages/21170773 can be found in the Developer's Guide.
Tag | Resolution |
---|---|
[:PhilipLightBaseURI:] | http://10.0.2.16/api/newdeveloper |
[:AA:] | Event.Argument |
[%:[:AA:].LightNumber.Value:%] | Runtime value of structure: Event.Argument.LightNumber.Value |
<on_true> and <on_false> | contains <on>true</on> and <on>false</on>; this is stop gap to avoid string to boolean conversion in TP |
[:@WFRT:] | Delay processing of Tags at Workflow execution time. Note the TQLEngine goes through number of processing / pre-processing phases. Prefixing it with WFRT tells the Engine to delay resolving the Tags until the execution of the workflow |
[%:[:AA:].state.On.Value:%] | Runtime value of structure: Event.Argument.state.On.Value |
<Include>[:@WFRT:]on_[%:[:AA:].state.On.Value:%]</Include> | Includes the content of either <on_true> or <on_false> |
4. Process the message received from the Philip Light Bridge
<Output name="Result" as="ActionResult"> <Value> <State> <On>[%:[:AA:].state.Value:%]</On> </State> </Value> </Output>
Associate Action with a ThingFacet Attribute
We will now have to associate a ThingFacet Attribute (State) with the Action using the KnownBy modifier. When State is "KnownBy" PhilipLightAction, any State value changes will activate the PhilipLightAction.
<ThingFacet Name="PhilipLightFacet"> ... <PhilipLightState Name="State" KnownBy="PhilipLightAction"/> </ThingFacet>
The attribute modifier update= "auto" makes sure that once the action associated with this attribute is triggered, its workflow continues to run and wait for subsequent sensor events (not just the first event). This modifier is only used with actionable attributes. For more details, refer to Automatic Action trigger.
Combining ThingFacet with a ThingModel
Finally, in order to use ThingFacet we have to combine it with a ThingModel. We define ThingModel to contain only a unique system identifier.
<ThingModel Name="PhilipLightModel" Combines="PhilipLightFacet"> <Sid Name="LightId"/> </ThingModel>
PhilipLightFacet can only be instantiated (and write TQL Queries) when it is combined with PhilipLightModel. PhilipLightModel will inherit all the attributes from PhilipLightFacet, in addition to its own attributes. The ThingFacet PhilipLightFacet hence serves as a reusable component.
More details on the use of "combines" can be found here. Information on Sid can be found here.
Initializing Lights
Typically ThingModels are instantiated by writing external Create or Save Queries. In this case the Lights data is itself coming from another Provider API. Number of device vendors follow the pattern of providing metadata loading APIs. This is a common approach. In PhilipLight example, we can load PhilipLightModel data by invoking HTTP Get on Lights. Let's create another ThingFacet and ThingModel for the purpose of loading the data. We also have to call the Create Query within the ThingFacet.
The building blocks to Loading Lights are:
- Ability to run Create TQL Query from ThingFacet
- Ability to Load lights at any given time
TQLEngine allows us to run TQL Queries from the Model itself using <OnRequest> Tag. We need to provide the Target ID to make this request. Since queries can be called any number of times, it is always a good idea to wrap calling of a TQL Query in a generic Macro Definition. Let's call this ExecuteQuery.
Generic Macro to Execute a TQL Query
<Macro Name="ExecuteQuery"> <Argument> <QueryString> <Query/> </QueryString> </Argument> <Result> <OnRequest> <Target>[:RuntimeParams.FacetIDName:]</Target> <Process> <Message> <Value>[:$Macro.Argument.QueryString:]</Value> </Message> </Process> </OnRequest> </Result> </Macro>
Macro to Create Light
Since creating of light can be called multiple times, we can wrap the creating of Light itself in another Macro definition called AddLight. In AddLight we can in turn use ExecuteQuery Macro.
<Macro Name="AddLight"> <Argument> <LightNumber/> <On/> <bri/> <hue/> <sat/> </Argument> <Result> <ExecuteQuery> <QueryString> <DeleteAll> <PhilipLightModel> <LightNumber Eq="[:$Macro.Argument.LightNumber:]"/> </PhilipLightModel> </DeleteAll> <Create> <PhilipLightModel> <LightNumber>[:$Macro.Argument.LightNumber:]</LightNumber> <State> <On>[:$Macro.Argument.On:]</On> <bri>[:$Macro.Argument.bri:]</bri> <hue>[:$Macro.Argument.hue:]</hue> <sat>[:$Macro.Argument.sat:]</sat> </State> </PhilipLightModel> </Create> </QueryString> </ExecuteQuery> </Result> </Macro>
ThingFacet to Load Lights
We need a trigger mechanism to load the lights. For this purpose we can write another PhilipLightManagerFacet ThingFacet with an LoadLightsAction Action against a LoadLights Attribute. This ThingFacet is similar to any other ThingFacet except that the Invoke is to the AddLight Macro using FacetScript.
<ThingFacet Name="PhilipLightManagerFacet"> <String Name="LoadLights" KnownBy="LoadLightsAction"/> <Action Name="LoadLightsAction"> <Workflow Limit="1" Live="1" Timeout="-1"> <Task Name="Main" While="True"> <Event Name="Argument" As="ActionArgument"/> <Invoke Name="GetLights" waitFor="ActionArgument" Get="[:PhilipLightBaseURI:]/lights"/> <Log Message="Lights Loaded...[:Invoke.GetLights.Message.Value:]"/> <Invoke Name="ParseGetLightsResult"> <FacetScript> <AddLight> <LightNumber>1</LightNumber> <On>[:Invoke.GetLights.Message.Value.1.State.On:]</On> <bri>[:Invoke.GetLights.Message.Value.1.State.bri:]</bri> <hue>[:Invoke.GetLights.Message.Value.1.State.hue:]</hue> <sat>[:Invoke.GetLights.Message.Value.1.State.sat:]</sat> </AddLight> <AddLight> <LightNumber>2</LightNumber> <On>[:Invoke.GetLights.Message.Value.2.State.On:]</On> <bri>[:Invoke.GetLights.Message.Value.2.State.bri:]</bri> <hue>[:Invoke.GetLights.Message.Value.2.State.hue:]</hue> <sat>[:Invoke.GetLights.Message.Value.2.State.sat:]</sat> </AddLight> <AddLight> <LightNumber>3</LightNumber> <On>[:Invoke.GetLights.Message.Value.3.State.On:]</On> <bri>[:Invoke.GetLights.Message.Value.3.State.bri:]</bri> <hue>[:Invoke.GetLights.Message.Value.3.State.hue:]</hue> <sat>[:Invoke.GetLights.Message.Value.3.State.sat:]</sat> </AddLight> </FacetScript> </Invoke> <Output Name="Result" As="ActionResult"> <Value> <LoadLights>True</LoadLights> </Value> </Output> </Task> </Workflow> </Action> </ThingFacet>
Let's combine PhilipLightManagerFacet with a PhilipLightManagerModel.
<ThingModel Name="PhilipLightManagerModel" Combines="PhilipLightManagerFacet"> <Sid Name="ManagerId"/> </ThingModel>
Query and subscription
Queries we need are:
- Trigger Loading of Lights using PhilipLightManagerModel
- Change the state of Light using PhilipLightModel
- Find the Light and Manager models.
Queries
<Query> <Find format="all"> <PhilipLightModel> <LightNumber eq="1"/> </PhilipLightModel> </Find> </Query>
<Query> <Find format="All"> <PhilipLightManagerModel> <ManagerId ne=""/> </PhilipLightManagerModel> </Find> </Query>
<Query> <DeleteAll> <PhilipLightManagerModel> <ManagerId ne=""/> </PhilipLightManagerModel> </DeleteAll> <Create> <PhilipLightManagerModel> <LoadLights> True </LoadLights> </PhilipLightManagerModel> </Create> </Query>
<Query> <Find format="All"> <PhilipLightModel> <LightNumber eq="1"/> </PhilipLightModel> </Find> <SetResponseData> <Key> Message.Value.Find.Result.PhilipLightModel.state.On.Value </Key> <Value> false </Value> </SetResponseData> <Update> <From> Result </From> <Include> $Response.Message.Value.Find </Include> </Update> </Query>