UI Simple
Micasaverde (Talk | contribs) |
|||
(18 intermediate revisions by 4 users not shown) | |||
Line 1: | Line 1: | ||
− | [[ | + | <br> NOTE: After reading this document to familiarize yourself with the concepts, see [[UI Simple Sample]] which has source code for a free implementation that you can embed within your applications to do all the work. |
− | + | ||
− | + | MIOS is a lightweight home automation system. The 'brain' of the MIOS software is the back-end, the engine, which runs stand-alone on a variety of internet-connected devices, such as PC's, Mac's, Wi-Fi access points, and dedicated home automation gateways. MIOS also includes a portal at mios.com, which acts as secure relay to MIOS systems that may be behind firewalls. Users can register for an account at mios.com, and that account can be linked to one or more MIOS systems to provide the user remote access to his MIOS system from anywhere. It is easy to control a MIOS system with simple http get's (normal internet requests). The URL you will open is generally data_request?id=xxx, where xxx is some sort of request or control command. | |
− | + | This document describes how to create a simple user interface to control a MIOS system using the simplified lu_sdata (LuaUPnP Simple Data) request. This document describes a simple control-only user interface, meaning it let's the user run scenes and control devices, but does not provide any means to change configuration or do advanced tasks. Whatever device is running the user interface (cell phone, web page, television, etc.) will be referred to as the "controller", and the MIOS engine, or system, that is being controlled by the controller is the "engine". | |
− | + | You can test out all the commands a normal web browser. For example, if your engine is on the same local network as your web browser, and your engine has the IP address: 192.168.2.150, you can view the status of all scenes and devices by opening this link in your browser: http://192.168.2.150:3480/data_request?id=lu_sdata and if you want to turn on device #5 you open this link in your browser: http://192.168.2.150:3480/data_request?id=lu_action&DeviceNum=5&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=1 | |
− | + | The user interface has only two screens, or modes: 1) Basic setup which only consists of identifying the engine the user will control, and 2) Normal usage which consists of running scenes (pre-defined groups of commands), and controlling devices. | |
− | + | A publicly accessible MiOS engine is available on the internet so you can test all these commands as you go. See the section at the end of this document. | |
− | 1 | + | == Mode 1: Basic setup and locating the engine == |
− | + | The first thing the user interface should do is find the engine on the internet. There are 2 different ways to control an engine: | |
− | + | 1) Directly, using an IP address, where the engine is either on the same local area network as the controller or has a static IP or port forward that's publicly accessible from the internet, such as in the example links above. | |
− | When the controller first starts, display this: | + | 2) Through one of the MIOS secure forward servers, which acts as a relay to an engine that may be behind a firewall. The URL is identical except that instead of using the IP you use a mios forward server followed by the /username/password/serial_number, where the username/password are from the user's mios.com account that he linked to his engine, and serial_number is the unique id of the engine. So, assuming you want to turn on device #5 on engine #10266 and the user linked it his mios account with the username "john" and password "tokyo", you would open this URL (note it's identical to the URL above): https://fwd2.mios.com/john/tokyo/10266/data_request?id=lu_action&DeviceNum=5&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=1 |
+ | |||
+ | NOTE: DeviceNum is the '''Device number''' of the device you want to control (found on the Settings tab: e.g. '''Device #5''') and not the ID number. | ||
+ | |||
+ | NOTE: Because method #2 is much slower than method #1 when both the engine and the controller are on the same network, it is generally preferred to use method #1 when the controller is in the home, and method #2 when the controller is away from the home and needs to talk to the engine through the homeowner's firewall. If you are putting your UI in something that is only on a local network, like a TV, and you do not want to give the user the ability to control an engine outside the home, you may only implement method #1. If the UI is running on something that a mobile phone that has NO wi-fi and can only connect through the mobile network, then you may want to implement method #2 only. Normally, though, a user interface should be able to use both method #1 and method #2, and this is how we spec it for our UI's. | ||
+ | |||
+ | When the controller first starts, display this: | ||
Enter your mios.com username: [__input_box__] [go] | Enter your mios.com username: [__input_box__] [go] | ||
__I don't have a mios.com account or I want to specify the IP address.__ | __I don't have a mios.com account or I want to specify the IP address.__ | ||
− | Where [go] is a button, [__input_box__] is for the user to type in the username, and the __I don't have... is a link, or button, or similar. | + | Where [go] is a button, [__input_box__] is for the user to type in the username, and the __I don't have... is a link, or button, or similar. If the user clicks the 'I don't have' link, display this: |
Enter the IP: [__input_box__] [go] | Enter the IP: [__input_box__] [go] | ||
− | When the user clicks 'go' attempt to the open this url: http://ip:3480/data_request?id=lu_alive (substitute the actual IP), and if you get back an "OK" in the response, store the IP address in your controller locally (ie a conf file, registry, etc) and continue to the next step. | + | When the user clicks 'go' attempt to the open this url: http://ip:3480/data_request?id=lu_alive (substitute the actual IP), and if you get back an "OK" in the response, store the IP address in your controller locally (ie a conf file, registry, etc) and continue to the next step. If you don't get an OK, display an error and go back. |
− | If the user supplied a mios.com username, open this URL: | + | If the user supplied a mios.com username, open this URL: https://sta1.mios.com/locator_json.php?username=user (substitute 'user' for the actual username). This will return a list of all the engines you can control, both with method #1 and with method #2, meaning you will see both engines on the local network which may or may not be tied to the user's mios.com account, and you will see engines tied to the mios.com account which may or may not be on the local network. As with most of the requests, the returned data is in JSON format. The data you get back is not formatted with new lines and spaces. If you want to be more human readable for debugging, copy/paste the results into http://jsonlint.com and it will format it nicely. The controller needs to have a json library so it can parse the json responses. The data you get back will be like this: |
"units": [ | "units": [ | ||
− | + | { | |
− | + | "serialNumber": "10266", | |
− | + | "FirmwareVersion": "1.1.1052", | |
− | + | "ipAddress": "192.168.2.117", | |
− | + | "name": "skyvera", | |
− | + | "users": [ | |
− | + | "skyvera", | |
− | + | "aaronb" | |
− | + | ], | |
− | + | "active_server": "fwd2.mios.com", | |
− | + | "forwardServers": [ | |
− | + | { | |
− | + | "hostName": "fwd2.mios.com", | |
− | + | "primary": true | |
− | + | }, | |
− | + | { | |
− | + | "hostName": "fwd1.mios.com", | |
− | + | "primary": false | |
− | + | } | |
− | + | ] | |
− | + | }, | |
− | + | { | |
− | + | "serialNumber": "8035", | |
− | + | "FirmwareVersion": "1.1.1047", | |
− | + | "ipAddress": "192.168.2.116", | |
− | + | "users": [ | |
− | + | "ovidiu", | |
− | + | "alfonsomios", | |
− | + | "aaronb", | |
− | + | "mrhtn" | |
− | + | ], | |
− | + | "active_server": "fwd1.mios.com", | |
− | + | "forwardServers": [ | |
− | + | { | |
− | + | "hostName": "fwd1.mios.com", | |
− | + | "primary": true | |
− | + | }, | |
− | + | { | |
− | + | "hostName": "fwd2.mios.com", | |
− | + | "primary": false | |
− | + | } | |
− | + | ] | |
− | + | }, | |
− | + | { | |
− | + | "serialNumber": "10516", | |
− | + | "FirmwareVersion": "1.1.1047", | |
− | + | "ipAddress": "192.168.2.23", | |
− | + | "users": [ | |
− | + | ||
− | + | ], | |
− | + | "active_server": "fwd2.mios.com", | |
− | + | "forwardServers": [ | |
− | + | { | |
− | + | "hostName": "fwd2.mios.com", | |
− | + | "primary": true | |
− | + | }, | |
− | + | { | |
− | + | "hostName": "fwd1.mios.com", | |
− | + | "primary": false | |
− | + | } | |
− | + | ] | |
− | + | } | |
− | + | ] | |
− | units is an array of JSON objects, one representing each engine. | + | units is an array of JSON objects, one representing each engine. If the tag ipAddress exists for an engine, that means it's available on the local network and can be controlled locally with method #1, otherwise the ipAddress tag will not exist. The users array is a list of all the mios.com usernames which have access to this engine. So, if the username you passed on the locator_json.php URL is also in the users array, then the engine can be controlled remotely with the username, using whatever server is listed in the "active_server" tag. The forwardServers tag lists the primary server for remote access and one or more backups. If the tag "name" exists, that is a name which the user assigned to his engine. |
− | + | If you cannot get a response from sta1.mios.com, try sta2.mios.com. They are mirrored servers with the same data but in different data centers for redundancy. | |
− | + | The next screen in the UI should say "What MiOS engine do you want to control?" and then display a list of all the engines with both the serialNumber and name if it exists. You should have 2 icons next to each for 'remote' and 'local' access. In the above example, assuming the username I passed in is "skyvera" 10516 will have the 'local' icon only, since there are no users, and for 8035 it will also be 'local' only because skyvera is not one of the allowed users. For 10266 display both the 'remote' and 'local' icon. At the bottom of the page you can have a legend: | |
− | [ | + | [R icon] - You can access this MiOS engine from anywhere in the world over the internet using your mios.com account |
− | + | [L icon] - You can access this MiOS engine locally on your home network without going through your mios.com account | |
− | At this point display: | + | And also a multiple choice asking the user if he wants: Automatic connection, Local connection only, or Remote connection only. |
+ | |||
+ | Let the user pick the engine he wants to control and store in the controller's local storage the username supplied by the user, and from the JSON file for whatever engine the user picked store the contents of serialNumber, ipAddress, active_server, and store the list of servers in forwardServers. | ||
+ | |||
+ | At this point display: | ||
mios.com password: [__input_box__] | mios.com password: [__input_box__] | ||
− | + | [ ] Store my password so I don't have to enter it each time | |
− | + | [go] | |
− | If the user checks the box, store the password. | + | If the user checks the box, store the password. When the user clicks go, if you stored an ipaddress, test it with: |
− | http://ip:3480/data_request?id=lu_alive | + | http://ip:3480/data_request?id=lu_alive |
− | and confirm you get an OK. | + | and confirm you get an OK. Then test the password with: |
− | https://xxx.mios.com/username/password/serial/data_request?id=lu_alive and substitute xxx.mios.com for the active_server, and substitute the actual username/password/serial. | + | https://xxx.mios.com/username/password/serial/data_request?id=lu_alive and substitute xxx.mios.com for the active_server, and substitute the actual username/password/serial. Again, confirm you get back an OK. |
− | If you do not get an OK for both (or the 2nd one if there was no local ip address), display an error and go back: | + | If you do not get an OK for both (or the 2nd one if there was no local ip address), display an error and go back: |
Unable to connect. Please check your password. [ok] | Unable to connect. Please check your password. [ok] | ||
− | == Mode 2: Normal operation == | + | == Mode 2: Normal operation == |
− | Now that you have stored the connection information (ip address, etc.), you can start the main application. | + | Now that you have stored the connection information (ip address, etc.), you can start the main application. From now on, whenever the user starts the UI, it should go straight into the main application and not ask him for the connection information again, although within the normal operation the user has the option of returning to setup. |
− | The use can choose to control ''devices'' and ''scenes''. | + | The use can choose to control ''devices'' and ''scenes''. A device is a light switch, a thermostat, a door lock, etc. Some devices may not have any control options, meaning the user cannot do anything with them, and they are only there to show status, such as a motion sensor or a temperature sensor. Scenes simply groups of actions the user has pre-defined, such as "Go to bed", which may be a scene that turns off all the lights. |
− | The user is able to create ''rooms'' and ''sections'' to organize the devices and scenes. | + | The user is able to create ''rooms'' and ''sections'' to organize the devices and scenes. Devices and scenes can be assigned to a room, and each room can be assigned to a section. Only very large home with many rooms divide the rooms into multiple sections, such as "West Wing", "3rd floor", etc. By default there is only 1 section and all the rooms belong to it. |
− | Devices and rooms may or may not be assigned to a room. | + | Devices and rooms may or may not be assigned to a room. Small installations with only a handful of devices typically do not create rooms, and all the devices and sections are left unassigned. When the user does create rooms, he does not need to assign all devices and sections to a room, so some may remain as unassigned. |
− | Each device has a category, such as 'dimmable light', 'thermostat', 'door lock', and so on. | + | Each device has a category, such as 'dimmable light', 'thermostat', 'door lock', and so on. |
− | So, a typical top-level menu may have the following options: | + | So, a typical top-level menu may have the following options: |
− | ''Setup'' to return to Mode 1 and choose a new engine to connect to. | + | ''Setup'' to return to Mode 1 and choose a new engine to connect to. If the user interface has a menu or option button the Setup can be there so it does not occupy space on the main menu. |
− | ''Scenes'' This lists all the scenes. | + | ''Scenes'' This lists all the scenes. First list the scenes not assigned to a room, then list the scenes grouped by the room they are assigned to. |
− | ''Lights'', ''Thermostats'', etc. | + | ''Lights'', ''Thermostats'', etc. Next the controller lists the categories so the user can just straight to 'thermostats' and see what thermostats, lights, etc. are in the house. The lu_sdata request will provide you with a list of the categories that are actually used in the system. Only display categories that are listed in the 'Status of the device or scene and control buttons' section below. In other words, if you have a device with category #1 (Interface), and there is a category #1 in the lu_sdata, do NOT display it in the user interface. Do not display a button 'Interface', and do NOT display the device with this category anywhere in the UI. Devices that are in the categories shown below should be ignored completely. |
− | ''Devices'' This lists all the devices that are not assigned to a room | + | ''Devices'' This lists all the devices that are not assigned to a room |
− | ''Living Room'', ''Bedroom'', etc. | + | ''Living Room'', ''Bedroom'', etc. Next is a list of all the rooms, broken down by section. Choosing a room lists the scenes in that room, followed by the devices. |
− | Therefore if a device is assigned to a room, it will appear in 2 places, in both the room and in the device's category. | + | Therefore if a device is assigned to a room, it will appear in 2 places, in both the room and in the device's category. The same is true for scenes. Here is a typical top-level menu: |
− | [[Image: | + | [[Image:Uispec mainmenu.png]] |
+ | If the user choose a room that has a binary light, a dimmable light, and a thermostat in it, following is what the sub-menu looks like: | ||
+ | [[Image:Uispec submenu.png]] | ||
+ | The name of the device or scene is the primary identifier along with an icon that depends on the device category. Each device has control options that are specific to the device's category, like 'on' and 'off' for binary light switches, and 'on', 'off' and a slider bar for dimmable light switches. The current status of the device should be indicated both by the status of the icon (light bulb yellow if it's on, gray if it's not), as well as by highlighting the control button corresponding to the status, for example, the 'on' button should be highlighted if the device is on. | ||
+ | Additionally, there should be a space in the device's control or scene's control that is reserved to indicate the 'state' of the device or scene. All devices and scenes can have 1 of the following 4 states: none (nothing is going on with the device or scene), pending (it's actively being controlled), success (the last action performed was successful), error (something is wrong with the device). And, for both scenes and devices there needs to be a way to display a short comment. For large controllers, like a TV, this may mean displaying the comments on the UI whenever they exist. For small displays, like mobile phones, it's possible to have the user touch the device to see more details, such as the comments. | ||
− | + | == Software architecture: Two threads with a polling loop == | |
− | + | Typically the software for the controller is written with 2 separate threads: 1) the back end: a background poll loop that continuously polls the engine for changes and which updates an object-oriented class structure containing the list of sections, rooms, scenes and devices, as well as the state of the engine (ip address, running state, etc.), and 2) the front end: the user interface which displays the data retrieved by the poll loop. | |
− | + | These should be separate as much as possible with separate classes. That way it's possible to reskin the user interface at some point without having to rewrite the poll loop. The poll loop has no user interface and simply runs as a separate thread and just continuously polls the engine. The user interface should be separate from the poll loop so it can be replaced easily. | |
+ | Both the back end and front end need to access the same object oriented class structure and need to be able to operate completely independently, so separate threads and mutex's are required. | ||
− | You should have a | + | Just like devices and scenes can have 4 different "states", the engine itself also has the same 4 states: none, pending, success, error. |
+ | |||
+ | As mentioned, the back end (polling loop) creates a class structure which the front end renders, and one of the things in the class structure is the state of the engine. This should be stored as a signed integer, and the initial value will be -2, which always means 'not connected'. When the front end (the user interface) starts up and sees the engine state is -2 (not connected) it should display a 'Please wait. Connecting...' message. | ||
+ | |||
+ | == lu_sdata: The polling loop == | ||
+ | |||
+ | The back end is simply a polling loop that continuously requests the "lu_sdata" data_request from the engine. This is done by fetching the following URL: http://ip/data_request?id=lu_sdata | ||
+ | |||
+ | As mentioned in the beginning, you can use the IP address of the engine directly and always use port 3480, like this: | ||
+ | |||
+ | http://192.168.2.117:3480/data_request?id=lu_sdata | ||
+ | |||
+ | or you can connect remotely through the forward servers, like this: | ||
+ | |||
+ | https://fwd2.mios.com/john/tokyo/10266/data_request?id=lu_sdata | ||
+ | |||
+ | You should always connect directly whenever possible. In addition to storing the state of the engine in a shared variable, the back end should also store in a variable if it is connected directly or remotely through the forward server, and the user interface will display an icon indicating if it is connected directly or remotely. If the direct connection fails, you should automatically switch over to the remote connection. Once the network settings have changed, for example, a mobile phone has switched from 3G to Wi-Fi, try again the direct connection. | ||
+ | |||
+ | To make development easier, if you are using Firefox, which has a built-in xml parser, you can add an "output_format=xml" to the URL and the engine will convert the json to xml, which Firefox renders nicely. This is only for testing. Your actual controller must always retrieve everything in the native json format. | ||
+ | |||
+ | For example, MIOS has an engine available to outside developers on a static IP (76.168.224.30). The following URL will show the lu_sdata reformatted as xml in Firefox: | ||
+ | |||
+ | http://76.168.224.30:3480/data_request?id=lu_sdata&output_format=xml | ||
+ | |||
+ | You can use this for developing your user interface too, just drop the &output_format=xml and if you want to format the JSON nicely, visit jsonlint.com. | ||
+ | |||
+ | Note the top-level object contains these tags: | ||
+ | |||
+ | full="1" loadtime="1282441735" dataversion="441736333" state="-1" comment="" | ||
+ | |||
+ | full is either 0 or 1, and 1 means lu_sdata has returned the complete list of sections, rooms, devices and scenes. If full is 0, the lu_sdata is only returning the devices and scenes which have changed since the last request. On subsequent calls to lu_sdata you will get the loadtime and dataversion back on the URL, like this: | ||
+ | |||
+ | http://76.168.224.30:3480/data_request?id=lu_sdata&loadtime=1282441735&dataversion=441736333 | ||
+ | |||
+ | The state is the state of the engine. It is the same as the state of the devices and scenes. See "List of states" below to see what the numbers mean. If the state is not 'none', the main menu should display at the top a red, green or blue icon (for error, success or pending states) and show the user what is in the comment tag. | ||
+ | |||
+ | Next are all the device categories listed in an array. Each category has a name and an id tag, like this: name="Thermostat" id="5". You will only see the categories for devices that exist in the MIOS installation. Always use the id for internal reference; the name changes based on the user's language. | ||
+ | |||
+ | Next are all the sections listed in an array. Each section has a name and an id tag, like this: name="My Home" id="1" Again, in most homes, there is only 1 section, and, if there is only 1 section, you do not need to show this to the user. | ||
+ | |||
+ | Next are the rooms. Again, they have a name and an id, and also a section which the room is in. | ||
+ | |||
+ | Next are the scenes. Scenes also have a name and an id. They also have an 'active' of either 0 or 1. If the active is '1', that means the scene is currently active, meaning all the devices are presently in the state the scene will set. So if the scene is 'go to bed', and it turns off all the lights, and if all the lights are currently off, 'active' will be 1 for the scene whether or not the user recently ran the scene. Scenes also have a state and comment (see state list below). The back end (polling loop) actually doesn't do anything with the active, state or comment other than store then in shared objects or variables that the front end (the user interface) uses to render the controls for the user. | ||
+ | |||
+ | Next are the devices. They also have the name, id, state and comment, plus they have a category. Additionally, they have extra tags which depend on the category of device. Light switches, for example, will have a 'status' tag and a value of 0 or 1 indicating if the light is off or on. Thermostats also have a 'temperature' tag. See the 'categories' section below. | ||
+ | |||
+ | The polling loop continues to repeat indefinitely as long as the application is running. Each time it passes the loadtime and dataversion of the prior request. Additionally, you should add a timeout argument to the url. This is the number of seconds that the URL will block waiting for a change. For example, this URL: | ||
+ | |||
+ | http://76.168.224.30:3480/data_request?id=lu_sdata&loadtime=1282441735&dataversion=441736333&timeout=60 | ||
+ | |||
+ | Will block for a maximum of 60 seconds if no devices or scenes have changed since the loadtime=1282441735&dataversion=441736333. The timeout should be as large as is reasonable to minimize the amount of traffic between the controller and the engine, but small enough to ensure the socket libraries in the controller do not time out and exit with an error before the 'timeout' number of seconds has passed. | ||
+ | |||
+ | Additionally you should add a minimumdelay to the argument which is the number of millseconds that the engine will block the request even if there are changes. For example: | ||
+ | |||
+ | http://76.168.224.30:3480/data_request?id=lu_sdata&loadtime=1282441735&dataversion=441736333&timeout=60&minimumdelay=2000 | ||
+ | |||
+ | means that the request will block for up to 60 seconds and if a change occurs after the request has been blocking 2 seconds, the request will return instantly. But if the change has already occurred or occurs in less than 2 seconds, the engine will make sure the request blocks at least 2 seconds before returning. The reason for this is that often there is a flurry of changes in rapid succession. For example if a user runs a scene that turns on 20 lights, without the minimumdelay, lu_sdata would be constantly incrementing the dataversion and returning immediately for several seconds while the scene is running, and it would max out the cpu of both the engine and the controller having constant polls. | ||
+ | |||
+ | If the last lu_sdata request failed to return anything, then you must introduce your own delay to prevent from bogging down the controller or engine in a constant polling loop. | ||
+ | |||
+ | On the initial request it is harmless to pass in 0's for the loadtime and dataversion. This is the same as omiting them and means you will receive the full set of data. Also, when the user has made changes to his configuration, like renaming a device, adding a device, scene or room, etc., the loadtime value will change and you will get a full set of data also (full=1). While the engine is running, status changes only, like a light going on and off, etc., will only increment the dataversion. So when you pass in a value for loadtime, if it still matches the current loadtime timestamp of the engine's configuration, then lu_sdata will not return sections, rooms, or categories, and it will only return the scenes and devices that have changed since the dataversion you passed in. It also will not return the name of the scene or device, since changing the name results in a new loadtime. | ||
+ | |||
+ | Thus the back end poll loop may want to set a 'full' variable to 1 whenever it receives a full set of data, in which case the front end user interface knows to re-render the entire UI. When the poll loop receives partial, or diff, data, it can simply update the internal object or variable that stores the device or scene and set a 'dirty' or 'updated' flag for that scene or device, which is how the front end user interface knows to update that scene or device if it's currently shown on the screen. So, when a device is turned on or off, the poll loop's pending lu_sdata will immediately return with the new state of the device, and if that device is shown on the user interface, the UI should immediately change to reflect the new status of the device. | ||
+ | |||
+ | Following is some pseudo-code showing how the poll loop would typically be written: | ||
+ | |||
+ | int LoadTime=0; | ||
+ | int DataVersion=0; | ||
+ | int DefaultTimeout=60; | ||
+ | int DefaultMinimumDelay=2000; | ||
+ | int CurrentMinimumDelay=0; | ||
+ | int CurrentSleep=2000; | ||
+ | int EngineState=-2; // Meaning we are not connected | ||
+ | string IpAddress; // Will be: "http://76.168.224.30:3480/" or maybe "https://fwd2.mios.com/john/tokyo/10266/" | ||
+ | int NumFailures=0; | ||
+ | |||
+ | while( Quit==false ) | ||
+ | { | ||
+ | URL = IpAddress + "data_request?id=lu_sdata&loadtime=" + LoadTime + "&dataversion=" + DataVersion + "&timeout=" + DefaultTimeout + | ||
+ | "&minimumdelay=" + CurrentMinimumDelay; | ||
+ | string Data = FetchURL(URL); | ||
+ | |||
+ | // If the request was successful, there will be something in Data | ||
+ | if( Data.IsEmpty() ) | ||
+ | { | ||
+ | // Be sure the user knows we're not connected | ||
+ | EngineState=-2; | ||
+ | |||
+ | NumFailures = NumFailures + 1; | ||
+ | if( NumFailures > MAX_FAILURES ) | ||
+ | { | ||
+ | CheckConnection(); | ||
+ | continue; | ||
+ | } | ||
+ | |||
+ | // The request failed, so sleep a couple seconds before trying again | ||
+ | Sleep(CurrentSleep); | ||
+ | |||
+ | // No need to introduce a minimum delay since this will be the first request | ||
+ | CurrentMinimumDelay=0; | ||
+ | |||
+ | // Try again | ||
+ | continue; | ||
+ | } | ||
+ | |||
+ | // So we have data. Parse it and update our variables, like the EngineState, LoadTime, DataVersion, and all the scenes, devices, etc. | ||
+ | ParseResponse(Data); | ||
+ | |||
+ | // We got valid data, so introduce the minimumdelay in case there's a flood of changes | ||
+ | CurrentMinimumDelay=DefaultMinimumDelay; | ||
+ | } | ||
+ | |||
+ | <br> The is not complete code, but rather just an outline of the polling process. The assumption here is that there is other initialization code which sets variables, like IpAddress, based on the information from Mode 1/Setup, namely the locating of the engine. while( Quit==false ) means this is a loop that keeps going until the wants to quit the application. In reality when the user chooses to quit you'll need to kill the thread since the FetchURL could be blocking for a while waiting for changes. | ||
+ | |||
+ | On the first pass URL will have loadtime=0 and dataversion=0, meaning we will always get the full list of sections, rooms, etc. So, when ParseResponse parses the json data, it will create all the objects and variables the front end will use to show this to the user. | ||
+ | |||
+ | If the fetching of the URL fails, you should set the engine status back to the initial value of -2, which means the user's UI will be interrupted so he cannot continue, and he'll have a modal 'Please wait... Connecting'. It is normal for the engine to become unavailable for up to 45 seconds or so. For example, if the user just installed some plugins, or saved a bunch of changes, the engine will go offline and stop responding while it's resetting. Therefore, when the fetching of the URL fails the first time you should display the 'Please wait' but don't treat it as an error. That's why we have a if( NumFailures > MAX_FAILURES ), and presumably, since we have a delay of 2 seconds for each request, MAX_FAILURES may be 30 if we want to go to an error condition after 60 seconds of failures. In reality, if FetchURL blocks for a while, having a fixed number of failures, like MAX_FAILURES, may not be good. If FetchURL blocked for 60 seconds, you wouldn't want the user to have a 'Please wait...' for up to 30 minutes. Also, the 'Please wait... Connecting' modal dialog needs to have a 'Quit' and 'Setup' option so the user can exit the app, or go back to the setup part to change his connection. CheckConnection is a function that should take care of a problem by reporting this to the user and letting him check his network settings. In reality, if FetchURL fails, and if you are currently connecting locally, and you are using a mobile phone or other device that can switch from a local connection like wi-fi to a remote connection like 3G, then if you are on a local connection and it suddenly drops, you should right away run a process that checks if the local connection is ok, and if not, switches to the remote connection (ie the https://fwd2.mios.com). Similarly, if the network settings have changed and you were on a remote connection, but now are on wi-fi, you should check if the local connection is ok. This is assuming the user chose the default 'Automatic connection' back on the setup connection page (see: "And also a multiple choice asking the user if he wants: Automatic connection, Local connection only, or Remote connection only."), otherwise you will need to stick to a remote or local connection only. | ||
+ | |||
+ | If the connection fails, you can reset the minimumdelay on the next request to 0, because there's no reason for the engine to wait a couple seconds before return on a lu_sdata request for initial data; you don't want to slow down the user's experience. But, you need to introduce your own sleep of a couple seconds between requests so you're not maxing out the cpu on the engine or the controller. | ||
+ | |||
+ | In this code, it's assumed ParseResponse is the function that parses the JSON data and creates all the objects and variables in memory which the front end will render for the user. If ParseResponse sees the JSON tag "full": 1, then it should purge it's list of devices, scenes, rooms, sections, categories, and rebuild them from scratch, and set a flag so the user interface will re-render the new data. ParseResponse also needs to set the dataversion and loadtime that are passed on the next request to whatever just came in on this request. ParseResponse should also set the variable with the state of the engine (EngineState in this example) to the 'state' tag that comes back in the json data. This way the user interface can show the actual state, or the 'Please wait... Connecting' if the state is -2 (ie the controller is not connected to the engine). | ||
+ | |||
+ | If we got a valid response, then the back end should not introduce its own delay, it should simply set the minimumdelay and let the engine handle the delay (CurrentMinimumDelay=DefaultMinimumDelay). This way, you will not introduce any unnecessary latency in the user interface. | ||
+ | |||
+ | == The front end user interface == | ||
+ | |||
+ | As mentioned earlier, the front end of the controller should be kept separate from the back end as much as possible so the user interface can be reskinned. It will, however, need to access the objects and variables created by the back end in order to render the UI. When the back end receives a full set of data from lu_sdata (ie full=1) a flag should be set which causes the front end to re-render the entire UI. You should not, however, lose the user's place. If the user had selected "Living Room" and had selected the "Overhead chandalier", then even if the front end does a full re-render, it should still preserve the user's place. This may not be possible since it's possible the device or room has since been deleted. So the front end does need to take account that during a full re-render, everything may change. | ||
+ | |||
+ | When the back end updates the current state variables for devices and scenes, the front end should update those devices or scenes if they are visible so the user sees the change. As mentioned before, the icon for the device as well as what control button is highlighted should change to reflect the current state. | ||
+ | |||
+ | If there is no physical exit button, then the top level menu needs to have an exit option. | ||
+ | |||
+ | When rendering the scenes and devices, what you show the user is the 'name'. However, what is used internally is the 'id', which is not shown to the user. | ||
+ | |||
+ | There are 2 things you show the user: 1) the state of every device, scene and engine (meaning if the device is busy or has an error condition), and 2) the status of the device or scene (meaning if the device is on or off or what temperature it is), and 3) the control buttons the user uses to control the device or scene. | ||
+ | |||
+ | Number 2 and 3 are really the same. The control buttons for a light switch (ie on and off) also show the status of the light switch (if it's on, then the 'on' button is highlighted). Additionally there may be icons and readouts for to indicate the status. What status a device has, what control buttons it has, and how to display to them user is specific to each category of device. These are explained in the following 2 sections. | ||
+ | |||
+ | == State of engine, device, or scene == | ||
+ | |||
+ | The engine itself as well as each device and scene have a 'state' tag which indicates if everything is ok. The lu_sdata returns a number from -1 to 6 to indicate the state. In the user interface, though, you several state numbers are treated the same so you actually have only 4 states to contend with. Note that for the engine only, we've added an extra state -2 which means that you can't talk to the engine, as explained earlier. The 4 states lu_sdata returns are: | ||
+ | |||
+ | NONE: state value is -1. This means there's nothing to report to the user. The device/scene/engine is working ok, and is not doing anything at the moment. | ||
+ | |||
+ | PENDING: state value is 0, 1, 5 or 6. All of those values mean the same 'pending' state. A pending state is always shown in blue. So for devices and scenes there should be a state indicator. When the engine itself has a pending state, display a prominent 'busy' indicator at the top of the UI. Note some of the commands might return errors when the state is busy depending on what device is being controlled and what the engine is busy doing. The value of the 'comment' for the engine, scene or device should be available to the user. | ||
+ | |||
+ | ERROR: state value is 2 or 3. In either case display a red indicator needs to be shown for the engine, scene or device indicating a failure, and display the comment. | ||
+ | |||
+ | SUCCESS: state value is 4. Display a green indicator and the comment. In general the success state is temporary. If you turn on a light, for example, the engine will report the state of the device as SUCCESS for about 30 seconds so the user can see that whatever he did went through ok, and then the state returns to NONE. | ||
+ | |||
+ | == Status of the device or scene and control buttons == | ||
+ | |||
+ | This depends on what category of device you are showing. The status tags returned by lu_sdata are different for a thermostat, which has status tags like mode, heatsp, coolsp, etc., than for a light switch with has the status tags level and status. Normally a control button, like the button that turns on a light, if bound to a status so the user can tell by whether the button is highlighted or not if the command is already active. For example, with a light switch, there is a 'status' tag that is 0 or 1 depending on if it's on or off. There are also 2 command buttons: on and off. Those 2 command buttons are bound to the status, meaning that if the status is 0, then the 'off' button should appear highlighted so the user can tell the light is already off. Each control button is tied to a command, like the 'on' button sends the 'on' command to the device. You should always send the command even if the command button is already highlighted. In other words, even if the status of a light is 0 (off) and the off button is already highlighted, when the user selects the off button, send the 'off' command regardless. You send commands by opening a URL on the engine, just like the poll loop does. The difference is that to run a command the data_request is generally "lu_action" instead of "lu_sdata". The engine is multi-threaded and can handle many requests at once. So you should not interrupt the background polling loop when you want to send a command. The polling loop will continue to block on the lu_sdata request, and, in parallel, the you will do a lu_action request when the user selects a command button. | ||
+ | |||
+ | When the user selects a control button for a device or scene, you will use lu_action to tell the engine what to do. In all cases you will get back either an OK or a JobID inside a response tag. If you don't, you will get back an error message which you should pass to the user. If you don't get anything back, retry the lu_action once every 2 seconds for up to 30 seconds, displaying an hour glass or busy indicator while you do, and, if, after 30 seconds you still cannot get a response to lu_action, report to the user that you lost contact with the engine. It is normal for lu_action to fail if the engine is reloading. So, for example, if the user clicks 'Lock' and you get back the error message "Lock operation is not available", then display a popup message in the user interface with an 'ERROR' icon and display the message "Lock operation is not available" with an 'OK' button. On the other hand if you do not get any response at all to lu_action, or get an html error, just keep retrying over and over until you either get a response or timeout. | ||
+ | |||
+ | You will always pass a 'service', an 'action', and optionally arguments on lu_action. So you should have a common function, like SendAction(string service, string action, string arguments), which has the logic of sending the command to the engine. To determine what service, action and arguments to use, you can 1) log in with ssh, the root password is the wi-fi password printed on the bottom, and type: ''tail -f /var/log/cmh/LuaUPnP.log | grep ^08'' and then control the device through the web UI. You will see in the console all the service/action/arguments for the commands you're sending. 2) You can also call the data_request lu_invoke, like ''http://__IP__:3480/data_request?id=invoke'' which has a human-readable list of devices and when you click them it shows you the service/action/arguments available. 3) If you understand UPNP terminology, you can go in the Web UI to Apps, Develop Apps, Luup files, and retrieve the actual UPNP XML files. The device files start with D_, and when you click the settings for a device in the web UI, you will see the name of the UPNP device file it uses. | ||
+ | |||
+ | When the user selects a control button, do not automatically show that button highlighted. For example, if a light is 'off' and the user selects 'on', give the user feedback with the 'on' button so he knows his selection was recognized, but do not automatically switch which button is highlighted. The background poll loop will get back a new 'status' for the device when the engine has completed turning off the light. As soon as you send the command with lu_action, the polling loop should immediately return and the state for the device or scene will change to 'PENDING'. This means the user should see the 'blue' busy indicator so he knows the engine is busy controlling the device. When the engine has succeeded in setting the device, the state will change to 'SUCCESS' and the status variable(s) will be changed, and that will cause the UI to re-render the control showing the new status. | ||
+ | |||
+ | Following are the categories, the status codes and the control buttons: | ||
+ | |||
+ | SCENE: There is simply a 'Run' button for the control. The status is active or not active (active=1 or active=0). Scenes only have a 'run' button as the action, and if the scene is active, simply show the 'run' button in a highlighted state. When selected use service "urn:micasaverde-com:serviceId:HomeAutomationGateway1" and action "RunScene" and pass the id of the scene as the SceneNum argument. For example, to run scene id 5, open this request: http://192.168.2.117:3480/data_request?id=lu_action&serviceId=urn:micasaverde-com:serviceId:HomeAutomationGateway1&action=RunScene&SceneNum=5 | ||
+ | |||
+ | For devices, refer to the 'category' tag for the device: | ||
+ | |||
+ | CATEGORY #2 - Dimmable Light: 'status' is 0 or 1 for off or on, and if status is 1, 'level' is a value from 0-100 to indicate the brightness. Buttons 'on' and 'off' should be bound to the 'status' for the device, and if selected, should send the service "urn:upnp-org:serviceId:SwitchPower1" action "SetTarget" with the argument "newTargetValue" is a value of 0 for off or 1 for on. There should also be a slider which represents the 'level' for the device from 0-100. If the user changes the level, send the service "urn:upnp-org:serviceId:Dimming1", action "SetLoadLevelTarget" and argument "newLoadlevelTarget" is 0-100. | ||
+ | |||
+ | CATEGORY #3 - Switch: Like dimmable light, but it only has 'status' | ||
+ | |||
+ | CATEGORY #4 - Security Sensor: 'tripped' is 0 or 1 to indicate if the sensor is tripped or not (1=tripped). 'armed' is 0 or 1 to indicate if it's in an armed state. There is a single control button 'ARM'. If the 'armed' status is 1, display the 'ARM' button in a highlighted state, otherwise in a normal state. When the 'ARM' button is selected, this is one case where you do not use lu_action; use lu_variableset instead. ARM is simply a flag in the engine, so use this: http://ip:3480/data_request?id=lu_variableset&DeviceNum=x&serviceId=urn:micasaverde-com:serviceId:SecuritySensor1&Variable=Armed&Value=y and replace x with the device id, and replace y with 0 if the armed status is 1, and 1 if it's 0, so the ARM button is a toggle. | ||
+ | |||
+ | CATEGORY #5 - Thermostat: 'fan' is 'auto' if the fan mode is off/normal or 'ContinuousOn' if the fan is always on. 'mode' indicates the mode of the thermostat with options being 'Off', 'HeatOn', 'CoolOn', 'AutoChangeOver'. 'hvacstate' indicates the current state of the thermostat, in other words, if it's in cool mode, it indicates if the compressor is actively cooling or if the set point is reached. Values are 'Idle', 'Heating', 'Cooling', 'FanOnly', 'PendingHeat', 'PendingCool', 'Vent'. 'temperature' is the current ambient temperature. 'heatsp' and 'coolsp' are the current heat/cool setpoints. The user indicates in their systems location preferences if they want english or metric, so just display the values as shown and do not worry about converting them. There are 4 mode buttons 'Off', 'Heat', 'Cool', and 'Auto' which should be bound to the 'mode' value, and when selected send the service "urn:upnp-org:serviceId:HVAC_UserOperatingMode1" and action "SetModeTarget" with the argument "NewModeTarget" has the values "Off", "HeatOn", "CoolOn" or "AutoChangeOver". Depending on the space on the user interface these can either be 4 buttons with the active mode button selected, or a drop-down that shows the active mode and let's the user change it. The fan is simply on or off, so just display a 'fan' button which is highlighted if the 'fan' variable is 1. Be sure it's separate from the 4 modes so it doesn't look like a 5th mode. If the user selects it, send the service "urn:upnp-org:serviceId:HVAC_FanOperatingMode1" and action "SetMode" with the argument "NewMode" has the values "Auto" for off or "ContinuousOn" for on. In other words, if the fan was off and the fan button is not highlighted, send the value "ContinuousOn" and if it was on, send the value "Auto" so it is a toggle. Display the values for 'heatsp' and 'coolsp', but if the user selects either one, display a drop-down or a pop-up or a keypad to let the user select a new temperature. Then send the service "urn:upnp-org:serviceId:TemperatureSetpoint1_Heat" or "urn:upnp-org:serviceId:TemperatureSetpoint1_Cool" with the action "SetCurrentSetpoint" and the argument "NewCurrentSetpoint" is whatever temperature the user choose. Display the value of the 'temperature' to the user as read only, since it's the ambient temperature. | ||
+ | |||
+ | CATEGORY #6 - Camera: These don't have a status. The control button is marked 'VIEW', but it doesn't send an action. It just displays an image viewer on the controller. The viewer should have a large JPEG image which you get by calling: http://ip:3480/data_request?id=cam_image&Device_Num=x where x is the device ID. This request returns a JPEG file. If the camera also has a status 'streaming' and the status is not empty, the the image viewer popup should display a 'LIVE VIDEO' button, which displays the streaming video instead. See the [[Remote Camera Streaming]] to learn how. Both the JPEG image viewer and the motion image viewer must have buttons for the user to control the pan/tilt/zoom of the camera. These buttons are L, R, U, D, +, - (for left, right, up, down, zoom in, zoom out). When those buttons are pressed, use the lu_action with the service "urn:micasaverde-com:serviceId:PanTiltZoom1" and the action "MoveLeft", "MoveRight", "MoveUp", "MoveDown", "ZoomIn" or "ZoomOut". There are no arguments for those actions. | ||
+ | |||
+ | CATEGORY #7 - Door lock: 'status' is 1 for locked or 0 for unlocked. Display a 'lock' and 'unlock' button which are bound to the 'status' variable. If selected, use the service "urn:micasaverde-com:serviceId:DoorLock1" action "SetTarget" and value newTargetValue which is 0 for unlocked and 1 for locked. | ||
+ | |||
+ | CATEGORY #8 - Window covering: same as Dimmable Light. | ||
+ | |||
+ | CATEGORY #16 - Humidity sensor: 'humidity' is the relative humidity from 0-100. Just display the 'humidity', there are no control buttons. | ||
+ | |||
+ | CATEGORY #17 - Temperature sensor: 'temperature' same as with a thermostat. Just display the 'temperature', there are no control buttons. | ||
+ | |||
+ | CATEGORY #18 - Light sensor: 'light' is a measurement of the ambient light in the room. There is no universal scale, so just display the number for the user. Just display the 'light', there are no control buttons. | ||
+ | |||
+ | CATEGORY #21 - Power meter: 'watts' is the current consumption in watts. Just display the 'watts', there are no control buttons. | ||
+ | |||
+ | Note: All devices, regardless of category, will have a 'watts' tag if we can measure the current consumption. | ||
+ | |||
+ | == Testing everything on a public MiOS engine == | ||
+ | |||
+ | '''<span style="color: rgb(255, 0, 0);">NOTE:</span> <span style="color: rgb(255, 0, 0);">demo.mios.com it's not available anymore</span>.''' | ||
+ | |||
+ | A MiOS engine is available on the internet with a static IP so you can use it to test your user interface both in local mode, as well as remote mode. The IP is demo.mios.com (76.168.224.30). So to test your application in local mode, just use that IP and you will talk to it as though it were on the local network. The port 3480 is publicly accessible like a device on the local network would be (see: http://76.168.224.30:3480/data_request?id=lu_sdata). You can also test it in remote mode with the username demomios password demomios123 and serial number 12082. So, you can retrieve the same data above with this URL in remote mode: https://fwd2.mios.com/demomios/demomios123/12082/data_request?id=lu_sdata | ||
+ | |||
+ | So, as a walkthrough to see how things work, first open up the regular MiOS web-based user interface that comes with the system at: http://demo.mios.com:8080/cmh/ Note that unlike the simple control-only user interface described in this document, this is a full user interface that let's you change configuration and do advanced things. It uses a different polling mechanism than the simple lu_sdata described here. | ||
+ | |||
+ | Now, in another tab, using Firefox, which has a built-in xml parser, open: http://demo.mios.com:3480/data_request?id=lu_sdata&output_format=xml NOTE: You will not use output_format=xml in your app, it's just to make it easier to see what's going if you have a Firefox browser. | ||
+ | |||
+ | Now, in another tab, open this URL, but substitute the dataversion and loadtime variables from the prior tab: http://demo.mios.com:3480/data_request?id=lu_sdata&output_format=xml&loadtime=1282601808&dataversion=601808034&minimumdelay=3000 | ||
+ | |||
+ | You should see that the page waits 3 seconds to load because the minimum delay is a minimum response time. Then you get an empty page without any data, and the tag full is 0. This means nothing has changed since the dataversion. If you change even 1 digit in the loadtime= on the URL, which is a timestamp of the configuration file, then you will get back the full data again because the lu_sdata requests sees that you do not have the current database. | ||
+ | |||
+ | Now put back the correct loadtime and dataversion and add &timeout=15 to the URL, like this: http://demo.mios.com:3480/data_request?id=lu_sdata&output_format=xml&loadtime=1282601808&dataversion=601808034&minimumdelay=3000&timeout=15 | ||
+ | |||
+ | You should see that it now waits 15 seconds, and again returns nothing. Now change the timeout to 60, and open the page again. During the next 60 seconds while the page is loading, go back to the MiOS user interface you first opened, and in the room "Patio" turn the 'White Light' on if it's off, or off it's on. The tab that's blocking on the lu_sdata should immediately return and give you this: <device id="3" status="1" state="1" comment="White Light: Sending the Z-Wave command after 0 retries"/> | ||
+ | |||
+ | This is what causes your user interface to update device id 3. The state is now '1', meaning it's pending or busy while the engine is turning on or off the device. The indicator should appear in blue at this point. Copy/paste the dataversion from the request back into the URL and request it again. This time you should see: <device id="3" status="0" state="4" comment=""/> | ||
+ | |||
+ | The state will be 4 if you do it soon, meaning your user interface should show the device with a green 'success' indicator, and if you do it again in 30 seconds, it will have a state of -1, meaning nothing. | ||
+ | |||
+ | To turn the device like your user interface will, open this: | ||
+ | |||
+ | http://demo.mios.com:3480/data_request?id=lu_action&DeviceNum=3&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=1 | ||
+ | |||
+ | change the =1 to an =0 to turn it off. Note that the response is a jobID. This means the engine is asynchronously turning on/off the device. | ||
+ | |||
+ | Now try to turn on or off device #26, like this: | ||
+ | |||
+ | http://demo.mios.com:3480/data_request?id=lu_action&DeviceNum=26&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0 | ||
+ | |||
+ | You will get back an error "ERROR: No implementation". In your user interface, this means if the user tries to control device #26, the user should get an error popup dialog box with that error message in it. | ||
+ | |||
+ | You can test all the controls in your user interface with this demo system since it has all the current device categories. The devices in the room "Patio" are real devices and will give you proper responses when you try to control them. All other devices are dummy devices that will return an error if you do anything with them. The 'White Light' is a functioning light that should go through the pending and success states when you control it. The red light is a light that is ok, but, every time you try to control it, the job will fail. So the user should see a blue pending for a while when trying to turn it on or off, which then goes red and stays there for several seconds with an error message, but, after a while, reverts back to a neutral state (-1). The device 'Bad Light' is in a perpetually bad state and should always appear as red. | ||
+ | |||
+ | In the patio the scenes 'Light on' and 'Light off' turn the white light on and off. One or the other scene will be 'active' depending on what state the light is in. | ||
+ | |||
+ | Please don't try to change the configuration settings on this demo system since it is used by several developers. If something is messed up on it, email support [at] micasaverde [dot] com and someone will restore it back to the default setting. | ||
+ | |||
+ | [[Category:Development]] |
Latest revision as of 22:00, 27 August 2013
NOTE: After reading this document to familiarize yourself with the concepts, see UI Simple Sample which has source code for a free implementation that you can embed within your applications to do all the work.
MIOS is a lightweight home automation system. The 'brain' of the MIOS software is the back-end, the engine, which runs stand-alone on a variety of internet-connected devices, such as PC's, Mac's, Wi-Fi access points, and dedicated home automation gateways. MIOS also includes a portal at mios.com, which acts as secure relay to MIOS systems that may be behind firewalls. Users can register for an account at mios.com, and that account can be linked to one or more MIOS systems to provide the user remote access to his MIOS system from anywhere. It is easy to control a MIOS system with simple http get's (normal internet requests). The URL you will open is generally data_request?id=xxx, where xxx is some sort of request or control command.
This document describes how to create a simple user interface to control a MIOS system using the simplified lu_sdata (LuaUPnP Simple Data) request. This document describes a simple control-only user interface, meaning it let's the user run scenes and control devices, but does not provide any means to change configuration or do advanced tasks. Whatever device is running the user interface (cell phone, web page, television, etc.) will be referred to as the "controller", and the MIOS engine, or system, that is being controlled by the controller is the "engine".
You can test out all the commands a normal web browser. For example, if your engine is on the same local network as your web browser, and your engine has the IP address: 192.168.2.150, you can view the status of all scenes and devices by opening this link in your browser: http://192.168.2.150:3480/data_request?id=lu_sdata and if you want to turn on device #5 you open this link in your browser: http://192.168.2.150:3480/data_request?id=lu_action&DeviceNum=5&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=1
The user interface has only two screens, or modes: 1) Basic setup which only consists of identifying the engine the user will control, and 2) Normal usage which consists of running scenes (pre-defined groups of commands), and controlling devices.
A publicly accessible MiOS engine is available on the internet so you can test all these commands as you go. See the section at the end of this document.
[edit] Mode 1: Basic setup and locating the engine
The first thing the user interface should do is find the engine on the internet. There are 2 different ways to control an engine:
1) Directly, using an IP address, where the engine is either on the same local area network as the controller or has a static IP or port forward that's publicly accessible from the internet, such as in the example links above.
2) Through one of the MIOS secure forward servers, which acts as a relay to an engine that may be behind a firewall. The URL is identical except that instead of using the IP you use a mios forward server followed by the /username/password/serial_number, where the username/password are from the user's mios.com account that he linked to his engine, and serial_number is the unique id of the engine. So, assuming you want to turn on device #5 on engine #10266 and the user linked it his mios account with the username "john" and password "tokyo", you would open this URL (note it's identical to the URL above): https://fwd2.mios.com/john/tokyo/10266/data_request?id=lu_action&DeviceNum=5&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=1
NOTE: DeviceNum is the Device number of the device you want to control (found on the Settings tab: e.g. Device #5) and not the ID number.
NOTE: Because method #2 is much slower than method #1 when both the engine and the controller are on the same network, it is generally preferred to use method #1 when the controller is in the home, and method #2 when the controller is away from the home and needs to talk to the engine through the homeowner's firewall. If you are putting your UI in something that is only on a local network, like a TV, and you do not want to give the user the ability to control an engine outside the home, you may only implement method #1. If the UI is running on something that a mobile phone that has NO wi-fi and can only connect through the mobile network, then you may want to implement method #2 only. Normally, though, a user interface should be able to use both method #1 and method #2, and this is how we spec it for our UI's.
When the controller first starts, display this:
Enter your mios.com username: [__input_box__] [go] __I don't have a mios.com account or I want to specify the IP address.__
Where [go] is a button, [__input_box__] is for the user to type in the username, and the __I don't have... is a link, or button, or similar. If the user clicks the 'I don't have' link, display this:
Enter the IP: [__input_box__] [go]
When the user clicks 'go' attempt to the open this url: http://ip:3480/data_request?id=lu_alive (substitute the actual IP), and if you get back an "OK" in the response, store the IP address in your controller locally (ie a conf file, registry, etc) and continue to the next step. If you don't get an OK, display an error and go back.
If the user supplied a mios.com username, open this URL: https://sta1.mios.com/locator_json.php?username=user (substitute 'user' for the actual username). This will return a list of all the engines you can control, both with method #1 and with method #2, meaning you will see both engines on the local network which may or may not be tied to the user's mios.com account, and you will see engines tied to the mios.com account which may or may not be on the local network. As with most of the requests, the returned data is in JSON format. The data you get back is not formatted with new lines and spaces. If you want to be more human readable for debugging, copy/paste the results into http://jsonlint.com and it will format it nicely. The controller needs to have a json library so it can parse the json responses. The data you get back will be like this:
"units": [ { "serialNumber": "10266", "FirmwareVersion": "1.1.1052", "ipAddress": "192.168.2.117", "name": "skyvera", "users": [ "skyvera", "aaronb" ], "active_server": "fwd2.mios.com", "forwardServers": [ { "hostName": "fwd2.mios.com", "primary": true }, { "hostName": "fwd1.mios.com", "primary": false } ] }, { "serialNumber": "8035", "FirmwareVersion": "1.1.1047", "ipAddress": "192.168.2.116", "users": [ "ovidiu", "alfonsomios", "aaronb", "mrhtn" ], "active_server": "fwd1.mios.com", "forwardServers": [ { "hostName": "fwd1.mios.com", "primary": true }, { "hostName": "fwd2.mios.com", "primary": false } ] }, { "serialNumber": "10516", "FirmwareVersion": "1.1.1047", "ipAddress": "192.168.2.23", "users": [ ], "active_server": "fwd2.mios.com", "forwardServers": [ { "hostName": "fwd2.mios.com", "primary": true }, { "hostName": "fwd1.mios.com", "primary": false } ] } ]
units is an array of JSON objects, one representing each engine. If the tag ipAddress exists for an engine, that means it's available on the local network and can be controlled locally with method #1, otherwise the ipAddress tag will not exist. The users array is a list of all the mios.com usernames which have access to this engine. So, if the username you passed on the locator_json.php URL is also in the users array, then the engine can be controlled remotely with the username, using whatever server is listed in the "active_server" tag. The forwardServers tag lists the primary server for remote access and one or more backups. If the tag "name" exists, that is a name which the user assigned to his engine.
If you cannot get a response from sta1.mios.com, try sta2.mios.com. They are mirrored servers with the same data but in different data centers for redundancy.
The next screen in the UI should say "What MiOS engine do you want to control?" and then display a list of all the engines with both the serialNumber and name if it exists. You should have 2 icons next to each for 'remote' and 'local' access. In the above example, assuming the username I passed in is "skyvera" 10516 will have the 'local' icon only, since there are no users, and for 8035 it will also be 'local' only because skyvera is not one of the allowed users. For 10266 display both the 'remote' and 'local' icon. At the bottom of the page you can have a legend:
[R icon] - You can access this MiOS engine from anywhere in the world over the internet using your mios.com account
[L icon] - You can access this MiOS engine locally on your home network without going through your mios.com account
And also a multiple choice asking the user if he wants: Automatic connection, Local connection only, or Remote connection only.
Let the user pick the engine he wants to control and store in the controller's local storage the username supplied by the user, and from the JSON file for whatever engine the user picked store the contents of serialNumber, ipAddress, active_server, and store the list of servers in forwardServers.
At this point display:
mios.com password: [__input_box__] [ ] Store my password so I don't have to enter it each time [go]
If the user checks the box, store the password. When the user clicks go, if you stored an ipaddress, test it with:
http://ip:3480/data_request?id=lu_alive
and confirm you get an OK. Then test the password with:
https://xxx.mios.com/username/password/serial/data_request?id=lu_alive and substitute xxx.mios.com for the active_server, and substitute the actual username/password/serial. Again, confirm you get back an OK.
If you do not get an OK for both (or the 2nd one if there was no local ip address), display an error and go back:
Unable to connect. Please check your password. [ok]
[edit] Mode 2: Normal operation
Now that you have stored the connection information (ip address, etc.), you can start the main application. From now on, whenever the user starts the UI, it should go straight into the main application and not ask him for the connection information again, although within the normal operation the user has the option of returning to setup.
The use can choose to control devices and scenes. A device is a light switch, a thermostat, a door lock, etc. Some devices may not have any control options, meaning the user cannot do anything with them, and they are only there to show status, such as a motion sensor or a temperature sensor. Scenes simply groups of actions the user has pre-defined, such as "Go to bed", which may be a scene that turns off all the lights.
The user is able to create rooms and sections to organize the devices and scenes. Devices and scenes can be assigned to a room, and each room can be assigned to a section. Only very large home with many rooms divide the rooms into multiple sections, such as "West Wing", "3rd floor", etc. By default there is only 1 section and all the rooms belong to it.
Devices and rooms may or may not be assigned to a room. Small installations with only a handful of devices typically do not create rooms, and all the devices and sections are left unassigned. When the user does create rooms, he does not need to assign all devices and sections to a room, so some may remain as unassigned.
Each device has a category, such as 'dimmable light', 'thermostat', 'door lock', and so on.
So, a typical top-level menu may have the following options:
Setup to return to Mode 1 and choose a new engine to connect to. If the user interface has a menu or option button the Setup can be there so it does not occupy space on the main menu.
Scenes This lists all the scenes. First list the scenes not assigned to a room, then list the scenes grouped by the room they are assigned to.
Lights, Thermostats, etc. Next the controller lists the categories so the user can just straight to 'thermostats' and see what thermostats, lights, etc. are in the house. The lu_sdata request will provide you with a list of the categories that are actually used in the system. Only display categories that are listed in the 'Status of the device or scene and control buttons' section below. In other words, if you have a device with category #1 (Interface), and there is a category #1 in the lu_sdata, do NOT display it in the user interface. Do not display a button 'Interface', and do NOT display the device with this category anywhere in the UI. Devices that are in the categories shown below should be ignored completely.
Devices This lists all the devices that are not assigned to a room
Living Room, Bedroom, etc. Next is a list of all the rooms, broken down by section. Choosing a room lists the scenes in that room, followed by the devices.
Therefore if a device is assigned to a room, it will appear in 2 places, in both the room and in the device's category. The same is true for scenes. Here is a typical top-level menu:
If the user choose a room that has a binary light, a dimmable light, and a thermostat in it, following is what the sub-menu looks like:
The name of the device or scene is the primary identifier along with an icon that depends on the device category. Each device has control options that are specific to the device's category, like 'on' and 'off' for binary light switches, and 'on', 'off' and a slider bar for dimmable light switches. The current status of the device should be indicated both by the status of the icon (light bulb yellow if it's on, gray if it's not), as well as by highlighting the control button corresponding to the status, for example, the 'on' button should be highlighted if the device is on.
Additionally, there should be a space in the device's control or scene's control that is reserved to indicate the 'state' of the device or scene. All devices and scenes can have 1 of the following 4 states: none (nothing is going on with the device or scene), pending (it's actively being controlled), success (the last action performed was successful), error (something is wrong with the device). And, for both scenes and devices there needs to be a way to display a short comment. For large controllers, like a TV, this may mean displaying the comments on the UI whenever they exist. For small displays, like mobile phones, it's possible to have the user touch the device to see more details, such as the comments.
[edit] Software architecture: Two threads with a polling loop
Typically the software for the controller is written with 2 separate threads: 1) the back end: a background poll loop that continuously polls the engine for changes and which updates an object-oriented class structure containing the list of sections, rooms, scenes and devices, as well as the state of the engine (ip address, running state, etc.), and 2) the front end: the user interface which displays the data retrieved by the poll loop.
These should be separate as much as possible with separate classes. That way it's possible to reskin the user interface at some point without having to rewrite the poll loop. The poll loop has no user interface and simply runs as a separate thread and just continuously polls the engine. The user interface should be separate from the poll loop so it can be replaced easily.
Both the back end and front end need to access the same object oriented class structure and need to be able to operate completely independently, so separate threads and mutex's are required.
Just like devices and scenes can have 4 different "states", the engine itself also has the same 4 states: none, pending, success, error.
As mentioned, the back end (polling loop) creates a class structure which the front end renders, and one of the things in the class structure is the state of the engine. This should be stored as a signed integer, and the initial value will be -2, which always means 'not connected'. When the front end (the user interface) starts up and sees the engine state is -2 (not connected) it should display a 'Please wait. Connecting...' message.
[edit] lu_sdata: The polling loop
The back end is simply a polling loop that continuously requests the "lu_sdata" data_request from the engine. This is done by fetching the following URL: http://ip/data_request?id=lu_sdata
As mentioned in the beginning, you can use the IP address of the engine directly and always use port 3480, like this:
http://192.168.2.117:3480/data_request?id=lu_sdata
or you can connect remotely through the forward servers, like this:
https://fwd2.mios.com/john/tokyo/10266/data_request?id=lu_sdata
You should always connect directly whenever possible. In addition to storing the state of the engine in a shared variable, the back end should also store in a variable if it is connected directly or remotely through the forward server, and the user interface will display an icon indicating if it is connected directly or remotely. If the direct connection fails, you should automatically switch over to the remote connection. Once the network settings have changed, for example, a mobile phone has switched from 3G to Wi-Fi, try again the direct connection.
To make development easier, if you are using Firefox, which has a built-in xml parser, you can add an "output_format=xml" to the URL and the engine will convert the json to xml, which Firefox renders nicely. This is only for testing. Your actual controller must always retrieve everything in the native json format.
For example, MIOS has an engine available to outside developers on a static IP (76.168.224.30). The following URL will show the lu_sdata reformatted as xml in Firefox:
http://76.168.224.30:3480/data_request?id=lu_sdata&output_format=xml
You can use this for developing your user interface too, just drop the &output_format=xml and if you want to format the JSON nicely, visit jsonlint.com.
Note the top-level object contains these tags:
full="1" loadtime="1282441735" dataversion="441736333" state="-1" comment=""
full is either 0 or 1, and 1 means lu_sdata has returned the complete list of sections, rooms, devices and scenes. If full is 0, the lu_sdata is only returning the devices and scenes which have changed since the last request. On subsequent calls to lu_sdata you will get the loadtime and dataversion back on the URL, like this:
http://76.168.224.30:3480/data_request?id=lu_sdata&loadtime=1282441735&dataversion=441736333
The state is the state of the engine. It is the same as the state of the devices and scenes. See "List of states" below to see what the numbers mean. If the state is not 'none', the main menu should display at the top a red, green or blue icon (for error, success or pending states) and show the user what is in the comment tag.
Next are all the device categories listed in an array. Each category has a name and an id tag, like this: name="Thermostat" id="5". You will only see the categories for devices that exist in the MIOS installation. Always use the id for internal reference; the name changes based on the user's language.
Next are all the sections listed in an array. Each section has a name and an id tag, like this: name="My Home" id="1" Again, in most homes, there is only 1 section, and, if there is only 1 section, you do not need to show this to the user.
Next are the rooms. Again, they have a name and an id, and also a section which the room is in.
Next are the scenes. Scenes also have a name and an id. They also have an 'active' of either 0 or 1. If the active is '1', that means the scene is currently active, meaning all the devices are presently in the state the scene will set. So if the scene is 'go to bed', and it turns off all the lights, and if all the lights are currently off, 'active' will be 1 for the scene whether or not the user recently ran the scene. Scenes also have a state and comment (see state list below). The back end (polling loop) actually doesn't do anything with the active, state or comment other than store then in shared objects or variables that the front end (the user interface) uses to render the controls for the user.
Next are the devices. They also have the name, id, state and comment, plus they have a category. Additionally, they have extra tags which depend on the category of device. Light switches, for example, will have a 'status' tag and a value of 0 or 1 indicating if the light is off or on. Thermostats also have a 'temperature' tag. See the 'categories' section below.
The polling loop continues to repeat indefinitely as long as the application is running. Each time it passes the loadtime and dataversion of the prior request. Additionally, you should add a timeout argument to the url. This is the number of seconds that the URL will block waiting for a change. For example, this URL:
Will block for a maximum of 60 seconds if no devices or scenes have changed since the loadtime=1282441735&dataversion=441736333. The timeout should be as large as is reasonable to minimize the amount of traffic between the controller and the engine, but small enough to ensure the socket libraries in the controller do not time out and exit with an error before the 'timeout' number of seconds has passed.
Additionally you should add a minimumdelay to the argument which is the number of millseconds that the engine will block the request even if there are changes. For example:
means that the request will block for up to 60 seconds and if a change occurs after the request has been blocking 2 seconds, the request will return instantly. But if the change has already occurred or occurs in less than 2 seconds, the engine will make sure the request blocks at least 2 seconds before returning. The reason for this is that often there is a flurry of changes in rapid succession. For example if a user runs a scene that turns on 20 lights, without the minimumdelay, lu_sdata would be constantly incrementing the dataversion and returning immediately for several seconds while the scene is running, and it would max out the cpu of both the engine and the controller having constant polls.
If the last lu_sdata request failed to return anything, then you must introduce your own delay to prevent from bogging down the controller or engine in a constant polling loop.
On the initial request it is harmless to pass in 0's for the loadtime and dataversion. This is the same as omiting them and means you will receive the full set of data. Also, when the user has made changes to his configuration, like renaming a device, adding a device, scene or room, etc., the loadtime value will change and you will get a full set of data also (full=1). While the engine is running, status changes only, like a light going on and off, etc., will only increment the dataversion. So when you pass in a value for loadtime, if it still matches the current loadtime timestamp of the engine's configuration, then lu_sdata will not return sections, rooms, or categories, and it will only return the scenes and devices that have changed since the dataversion you passed in. It also will not return the name of the scene or device, since changing the name results in a new loadtime.
Thus the back end poll loop may want to set a 'full' variable to 1 whenever it receives a full set of data, in which case the front end user interface knows to re-render the entire UI. When the poll loop receives partial, or diff, data, it can simply update the internal object or variable that stores the device or scene and set a 'dirty' or 'updated' flag for that scene or device, which is how the front end user interface knows to update that scene or device if it's currently shown on the screen. So, when a device is turned on or off, the poll loop's pending lu_sdata will immediately return with the new state of the device, and if that device is shown on the user interface, the UI should immediately change to reflect the new status of the device.
Following is some pseudo-code showing how the poll loop would typically be written:
int LoadTime=0; int DataVersion=0; int DefaultTimeout=60; int DefaultMinimumDelay=2000; int CurrentMinimumDelay=0; int CurrentSleep=2000; int EngineState=-2; // Meaning we are not connected string IpAddress; // Will be: "http://76.168.224.30:3480/" or maybe "https://fwd2.mios.com/john/tokyo/10266/" int NumFailures=0; while( Quit==false ) { URL = IpAddress + "data_request?id=lu_sdata&loadtime=" + LoadTime + "&dataversion=" + DataVersion + "&timeout=" + DefaultTimeout + "&minimumdelay=" + CurrentMinimumDelay; string Data = FetchURL(URL); // If the request was successful, there will be something in Data if( Data.IsEmpty() ) { // Be sure the user knows we're not connected EngineState=-2; NumFailures = NumFailures + 1; if( NumFailures > MAX_FAILURES ) { CheckConnection(); continue; } // The request failed, so sleep a couple seconds before trying again Sleep(CurrentSleep); // No need to introduce a minimum delay since this will be the first request CurrentMinimumDelay=0; // Try again continue; } // So we have data. Parse it and update our variables, like the EngineState, LoadTime, DataVersion, and all the scenes, devices, etc. ParseResponse(Data); // We got valid data, so introduce the minimumdelay in case there's a flood of changes CurrentMinimumDelay=DefaultMinimumDelay; }
The is not complete code, but rather just an outline of the polling process. The assumption here is that there is other initialization code which sets variables, like IpAddress, based on the information from Mode 1/Setup, namely the locating of the engine. while( Quit==false ) means this is a loop that keeps going until the wants to quit the application. In reality when the user chooses to quit you'll need to kill the thread since the FetchURL could be blocking for a while waiting for changes.
On the first pass URL will have loadtime=0 and dataversion=0, meaning we will always get the full list of sections, rooms, etc. So, when ParseResponse parses the json data, it will create all the objects and variables the front end will use to show this to the user.
If the fetching of the URL fails, you should set the engine status back to the initial value of -2, which means the user's UI will be interrupted so he cannot continue, and he'll have a modal 'Please wait... Connecting'. It is normal for the engine to become unavailable for up to 45 seconds or so. For example, if the user just installed some plugins, or saved a bunch of changes, the engine will go offline and stop responding while it's resetting. Therefore, when the fetching of the URL fails the first time you should display the 'Please wait' but don't treat it as an error. That's why we have a if( NumFailures > MAX_FAILURES ), and presumably, since we have a delay of 2 seconds for each request, MAX_FAILURES may be 30 if we want to go to an error condition after 60 seconds of failures. In reality, if FetchURL blocks for a while, having a fixed number of failures, like MAX_FAILURES, may not be good. If FetchURL blocked for 60 seconds, you wouldn't want the user to have a 'Please wait...' for up to 30 minutes. Also, the 'Please wait... Connecting' modal dialog needs to have a 'Quit' and 'Setup' option so the user can exit the app, or go back to the setup part to change his connection. CheckConnection is a function that should take care of a problem by reporting this to the user and letting him check his network settings. In reality, if FetchURL fails, and if you are currently connecting locally, and you are using a mobile phone or other device that can switch from a local connection like wi-fi to a remote connection like 3G, then if you are on a local connection and it suddenly drops, you should right away run a process that checks if the local connection is ok, and if not, switches to the remote connection (ie the https://fwd2.mios.com). Similarly, if the network settings have changed and you were on a remote connection, but now are on wi-fi, you should check if the local connection is ok. This is assuming the user chose the default 'Automatic connection' back on the setup connection page (see: "And also a multiple choice asking the user if he wants: Automatic connection, Local connection only, or Remote connection only."), otherwise you will need to stick to a remote or local connection only.
If the connection fails, you can reset the minimumdelay on the next request to 0, because there's no reason for the engine to wait a couple seconds before return on a lu_sdata request for initial data; you don't want to slow down the user's experience. But, you need to introduce your own sleep of a couple seconds between requests so you're not maxing out the cpu on the engine or the controller.
In this code, it's assumed ParseResponse is the function that parses the JSON data and creates all the objects and variables in memory which the front end will render for the user. If ParseResponse sees the JSON tag "full": 1, then it should purge it's list of devices, scenes, rooms, sections, categories, and rebuild them from scratch, and set a flag so the user interface will re-render the new data. ParseResponse also needs to set the dataversion and loadtime that are passed on the next request to whatever just came in on this request. ParseResponse should also set the variable with the state of the engine (EngineState in this example) to the 'state' tag that comes back in the json data. This way the user interface can show the actual state, or the 'Please wait... Connecting' if the state is -2 (ie the controller is not connected to the engine).
If we got a valid response, then the back end should not introduce its own delay, it should simply set the minimumdelay and let the engine handle the delay (CurrentMinimumDelay=DefaultMinimumDelay). This way, you will not introduce any unnecessary latency in the user interface.
[edit] The front end user interface
As mentioned earlier, the front end of the controller should be kept separate from the back end as much as possible so the user interface can be reskinned. It will, however, need to access the objects and variables created by the back end in order to render the UI. When the back end receives a full set of data from lu_sdata (ie full=1) a flag should be set which causes the front end to re-render the entire UI. You should not, however, lose the user's place. If the user had selected "Living Room" and had selected the "Overhead chandalier", then even if the front end does a full re-render, it should still preserve the user's place. This may not be possible since it's possible the device or room has since been deleted. So the front end does need to take account that during a full re-render, everything may change.
When the back end updates the current state variables for devices and scenes, the front end should update those devices or scenes if they are visible so the user sees the change. As mentioned before, the icon for the device as well as what control button is highlighted should change to reflect the current state.
If there is no physical exit button, then the top level menu needs to have an exit option.
When rendering the scenes and devices, what you show the user is the 'name'. However, what is used internally is the 'id', which is not shown to the user.
There are 2 things you show the user: 1) the state of every device, scene and engine (meaning if the device is busy or has an error condition), and 2) the status of the device or scene (meaning if the device is on or off or what temperature it is), and 3) the control buttons the user uses to control the device or scene.
Number 2 and 3 are really the same. The control buttons for a light switch (ie on and off) also show the status of the light switch (if it's on, then the 'on' button is highlighted). Additionally there may be icons and readouts for to indicate the status. What status a device has, what control buttons it has, and how to display to them user is specific to each category of device. These are explained in the following 2 sections.
[edit] State of engine, device, or scene
The engine itself as well as each device and scene have a 'state' tag which indicates if everything is ok. The lu_sdata returns a number from -1 to 6 to indicate the state. In the user interface, though, you several state numbers are treated the same so you actually have only 4 states to contend with. Note that for the engine only, we've added an extra state -2 which means that you can't talk to the engine, as explained earlier. The 4 states lu_sdata returns are:
NONE: state value is -1. This means there's nothing to report to the user. The device/scene/engine is working ok, and is not doing anything at the moment.
PENDING: state value is 0, 1, 5 or 6. All of those values mean the same 'pending' state. A pending state is always shown in blue. So for devices and scenes there should be a state indicator. When the engine itself has a pending state, display a prominent 'busy' indicator at the top of the UI. Note some of the commands might return errors when the state is busy depending on what device is being controlled and what the engine is busy doing. The value of the 'comment' for the engine, scene or device should be available to the user.
ERROR: state value is 2 or 3. In either case display a red indicator needs to be shown for the engine, scene or device indicating a failure, and display the comment.
SUCCESS: state value is 4. Display a green indicator and the comment. In general the success state is temporary. If you turn on a light, for example, the engine will report the state of the device as SUCCESS for about 30 seconds so the user can see that whatever he did went through ok, and then the state returns to NONE.
[edit] Status of the device or scene and control buttons
This depends on what category of device you are showing. The status tags returned by lu_sdata are different for a thermostat, which has status tags like mode, heatsp, coolsp, etc., than for a light switch with has the status tags level and status. Normally a control button, like the button that turns on a light, if bound to a status so the user can tell by whether the button is highlighted or not if the command is already active. For example, with a light switch, there is a 'status' tag that is 0 or 1 depending on if it's on or off. There are also 2 command buttons: on and off. Those 2 command buttons are bound to the status, meaning that if the status is 0, then the 'off' button should appear highlighted so the user can tell the light is already off. Each control button is tied to a command, like the 'on' button sends the 'on' command to the device. You should always send the command even if the command button is already highlighted. In other words, even if the status of a light is 0 (off) and the off button is already highlighted, when the user selects the off button, send the 'off' command regardless. You send commands by opening a URL on the engine, just like the poll loop does. The difference is that to run a command the data_request is generally "lu_action" instead of "lu_sdata". The engine is multi-threaded and can handle many requests at once. So you should not interrupt the background polling loop when you want to send a command. The polling loop will continue to block on the lu_sdata request, and, in parallel, the you will do a lu_action request when the user selects a command button.
When the user selects a control button for a device or scene, you will use lu_action to tell the engine what to do. In all cases you will get back either an OK or a JobID inside a response tag. If you don't, you will get back an error message which you should pass to the user. If you don't get anything back, retry the lu_action once every 2 seconds for up to 30 seconds, displaying an hour glass or busy indicator while you do, and, if, after 30 seconds you still cannot get a response to lu_action, report to the user that you lost contact with the engine. It is normal for lu_action to fail if the engine is reloading. So, for example, if the user clicks 'Lock' and you get back the error message "Lock operation is not available", then display a popup message in the user interface with an 'ERROR' icon and display the message "Lock operation is not available" with an 'OK' button. On the other hand if you do not get any response at all to lu_action, or get an html error, just keep retrying over and over until you either get a response or timeout.
You will always pass a 'service', an 'action', and optionally arguments on lu_action. So you should have a common function, like SendAction(string service, string action, string arguments), which has the logic of sending the command to the engine. To determine what service, action and arguments to use, you can 1) log in with ssh, the root password is the wi-fi password printed on the bottom, and type: tail -f /var/log/cmh/LuaUPnP.log | grep ^08 and then control the device through the web UI. You will see in the console all the service/action/arguments for the commands you're sending. 2) You can also call the data_request lu_invoke, like http://__IP__:3480/data_request?id=invoke which has a human-readable list of devices and when you click them it shows you the service/action/arguments available. 3) If you understand UPNP terminology, you can go in the Web UI to Apps, Develop Apps, Luup files, and retrieve the actual UPNP XML files. The device files start with D_, and when you click the settings for a device in the web UI, you will see the name of the UPNP device file it uses.
When the user selects a control button, do not automatically show that button highlighted. For example, if a light is 'off' and the user selects 'on', give the user feedback with the 'on' button so he knows his selection was recognized, but do not automatically switch which button is highlighted. The background poll loop will get back a new 'status' for the device when the engine has completed turning off the light. As soon as you send the command with lu_action, the polling loop should immediately return and the state for the device or scene will change to 'PENDING'. This means the user should see the 'blue' busy indicator so he knows the engine is busy controlling the device. When the engine has succeeded in setting the device, the state will change to 'SUCCESS' and the status variable(s) will be changed, and that will cause the UI to re-render the control showing the new status.
Following are the categories, the status codes and the control buttons:
SCENE: There is simply a 'Run' button for the control. The status is active or not active (active=1 or active=0). Scenes only have a 'run' button as the action, and if the scene is active, simply show the 'run' button in a highlighted state. When selected use service "urn:micasaverde-com:serviceId:HomeAutomationGateway1" and action "RunScene" and pass the id of the scene as the SceneNum argument. For example, to run scene id 5, open this request: http://192.168.2.117:3480/data_request?id=lu_action&serviceId=urn:micasaverde-com:serviceId:HomeAutomationGateway1&action=RunScene&SceneNum=5
For devices, refer to the 'category' tag for the device:
CATEGORY #2 - Dimmable Light: 'status' is 0 or 1 for off or on, and if status is 1, 'level' is a value from 0-100 to indicate the brightness. Buttons 'on' and 'off' should be bound to the 'status' for the device, and if selected, should send the service "urn:upnp-org:serviceId:SwitchPower1" action "SetTarget" with the argument "newTargetValue" is a value of 0 for off or 1 for on. There should also be a slider which represents the 'level' for the device from 0-100. If the user changes the level, send the service "urn:upnp-org:serviceId:Dimming1", action "SetLoadLevelTarget" and argument "newLoadlevelTarget" is 0-100.
CATEGORY #3 - Switch: Like dimmable light, but it only has 'status'
CATEGORY #4 - Security Sensor: 'tripped' is 0 or 1 to indicate if the sensor is tripped or not (1=tripped). 'armed' is 0 or 1 to indicate if it's in an armed state. There is a single control button 'ARM'. If the 'armed' status is 1, display the 'ARM' button in a highlighted state, otherwise in a normal state. When the 'ARM' button is selected, this is one case where you do not use lu_action; use lu_variableset instead. ARM is simply a flag in the engine, so use this: http://ip:3480/data_request?id=lu_variableset&DeviceNum=x&serviceId=urn:micasaverde-com:serviceId:SecuritySensor1&Variable=Armed&Value=y and replace x with the device id, and replace y with 0 if the armed status is 1, and 1 if it's 0, so the ARM button is a toggle.
CATEGORY #5 - Thermostat: 'fan' is 'auto' if the fan mode is off/normal or 'ContinuousOn' if the fan is always on. 'mode' indicates the mode of the thermostat with options being 'Off', 'HeatOn', 'CoolOn', 'AutoChangeOver'. 'hvacstate' indicates the current state of the thermostat, in other words, if it's in cool mode, it indicates if the compressor is actively cooling or if the set point is reached. Values are 'Idle', 'Heating', 'Cooling', 'FanOnly', 'PendingHeat', 'PendingCool', 'Vent'. 'temperature' is the current ambient temperature. 'heatsp' and 'coolsp' are the current heat/cool setpoints. The user indicates in their systems location preferences if they want english or metric, so just display the values as shown and do not worry about converting them. There are 4 mode buttons 'Off', 'Heat', 'Cool', and 'Auto' which should be bound to the 'mode' value, and when selected send the service "urn:upnp-org:serviceId:HVAC_UserOperatingMode1" and action "SetModeTarget" with the argument "NewModeTarget" has the values "Off", "HeatOn", "CoolOn" or "AutoChangeOver". Depending on the space on the user interface these can either be 4 buttons with the active mode button selected, or a drop-down that shows the active mode and let's the user change it. The fan is simply on or off, so just display a 'fan' button which is highlighted if the 'fan' variable is 1. Be sure it's separate from the 4 modes so it doesn't look like a 5th mode. If the user selects it, send the service "urn:upnp-org:serviceId:HVAC_FanOperatingMode1" and action "SetMode" with the argument "NewMode" has the values "Auto" for off or "ContinuousOn" for on. In other words, if the fan was off and the fan button is not highlighted, send the value "ContinuousOn" and if it was on, send the value "Auto" so it is a toggle. Display the values for 'heatsp' and 'coolsp', but if the user selects either one, display a drop-down or a pop-up or a keypad to let the user select a new temperature. Then send the service "urn:upnp-org:serviceId:TemperatureSetpoint1_Heat" or "urn:upnp-org:serviceId:TemperatureSetpoint1_Cool" with the action "SetCurrentSetpoint" and the argument "NewCurrentSetpoint" is whatever temperature the user choose. Display the value of the 'temperature' to the user as read only, since it's the ambient temperature.
CATEGORY #6 - Camera: These don't have a status. The control button is marked 'VIEW', but it doesn't send an action. It just displays an image viewer on the controller. The viewer should have a large JPEG image which you get by calling: http://ip:3480/data_request?id=cam_image&Device_Num=x where x is the device ID. This request returns a JPEG file. If the camera also has a status 'streaming' and the status is not empty, the the image viewer popup should display a 'LIVE VIDEO' button, which displays the streaming video instead. See the Remote Camera Streaming to learn how. Both the JPEG image viewer and the motion image viewer must have buttons for the user to control the pan/tilt/zoom of the camera. These buttons are L, R, U, D, +, - (for left, right, up, down, zoom in, zoom out). When those buttons are pressed, use the lu_action with the service "urn:micasaverde-com:serviceId:PanTiltZoom1" and the action "MoveLeft", "MoveRight", "MoveUp", "MoveDown", "ZoomIn" or "ZoomOut". There are no arguments for those actions.
CATEGORY #7 - Door lock: 'status' is 1 for locked or 0 for unlocked. Display a 'lock' and 'unlock' button which are bound to the 'status' variable. If selected, use the service "urn:micasaverde-com:serviceId:DoorLock1" action "SetTarget" and value newTargetValue which is 0 for unlocked and 1 for locked.
CATEGORY #8 - Window covering: same as Dimmable Light.
CATEGORY #16 - Humidity sensor: 'humidity' is the relative humidity from 0-100. Just display the 'humidity', there are no control buttons.
CATEGORY #17 - Temperature sensor: 'temperature' same as with a thermostat. Just display the 'temperature', there are no control buttons.
CATEGORY #18 - Light sensor: 'light' is a measurement of the ambient light in the room. There is no universal scale, so just display the number for the user. Just display the 'light', there are no control buttons.
CATEGORY #21 - Power meter: 'watts' is the current consumption in watts. Just display the 'watts', there are no control buttons.
Note: All devices, regardless of category, will have a 'watts' tag if we can measure the current consumption.
[edit] Testing everything on a public MiOS engine
NOTE: demo.mios.com it's not available anymore.
A MiOS engine is available on the internet with a static IP so you can use it to test your user interface both in local mode, as well as remote mode. The IP is demo.mios.com (76.168.224.30). So to test your application in local mode, just use that IP and you will talk to it as though it were on the local network. The port 3480 is publicly accessible like a device on the local network would be (see: http://76.168.224.30:3480/data_request?id=lu_sdata). You can also test it in remote mode with the username demomios password demomios123 and serial number 12082. So, you can retrieve the same data above with this URL in remote mode: https://fwd2.mios.com/demomios/demomios123/12082/data_request?id=lu_sdata
So, as a walkthrough to see how things work, first open up the regular MiOS web-based user interface that comes with the system at: http://demo.mios.com:8080/cmh/ Note that unlike the simple control-only user interface described in this document, this is a full user interface that let's you change configuration and do advanced things. It uses a different polling mechanism than the simple lu_sdata described here.
Now, in another tab, using Firefox, which has a built-in xml parser, open: http://demo.mios.com:3480/data_request?id=lu_sdata&output_format=xml NOTE: You will not use output_format=xml in your app, it's just to make it easier to see what's going if you have a Firefox browser.
Now, in another tab, open this URL, but substitute the dataversion and loadtime variables from the prior tab: http://demo.mios.com:3480/data_request?id=lu_sdata&output_format=xml&loadtime=1282601808&dataversion=601808034&minimumdelay=3000
You should see that the page waits 3 seconds to load because the minimum delay is a minimum response time. Then you get an empty page without any data, and the tag full is 0. This means nothing has changed since the dataversion. If you change even 1 digit in the loadtime= on the URL, which is a timestamp of the configuration file, then you will get back the full data again because the lu_sdata requests sees that you do not have the current database.
Now put back the correct loadtime and dataversion and add &timeout=15 to the URL, like this: http://demo.mios.com:3480/data_request?id=lu_sdata&output_format=xml&loadtime=1282601808&dataversion=601808034&minimumdelay=3000&timeout=15
You should see that it now waits 15 seconds, and again returns nothing. Now change the timeout to 60, and open the page again. During the next 60 seconds while the page is loading, go back to the MiOS user interface you first opened, and in the room "Patio" turn the 'White Light' on if it's off, or off it's on. The tab that's blocking on the lu_sdata should immediately return and give you this: <device id="3" status="1" state="1" comment="White Light: Sending the Z-Wave command after 0 retries"/>
This is what causes your user interface to update device id 3. The state is now '1', meaning it's pending or busy while the engine is turning on or off the device. The indicator should appear in blue at this point. Copy/paste the dataversion from the request back into the URL and request it again. This time you should see: <device id="3" status="0" state="4" comment=""/>
The state will be 4 if you do it soon, meaning your user interface should show the device with a green 'success' indicator, and if you do it again in 30 seconds, it will have a state of -1, meaning nothing.
To turn the device like your user interface will, open this:
change the =1 to an =0 to turn it off. Note that the response is a jobID. This means the engine is asynchronously turning on/off the device.
Now try to turn on or off device #26, like this:
You will get back an error "ERROR: No implementation". In your user interface, this means if the user tries to control device #26, the user should get an error popup dialog box with that error message in it.
You can test all the controls in your user interface with this demo system since it has all the current device categories. The devices in the room "Patio" are real devices and will give you proper responses when you try to control them. All other devices are dummy devices that will return an error if you do anything with them. The 'White Light' is a functioning light that should go through the pending and success states when you control it. The red light is a light that is ok, but, every time you try to control it, the job will fail. So the user should see a blue pending for a while when trying to turn it on or off, which then goes red and stays there for several seconds with an error message, but, after a while, reverts back to a neutral state (-1). The device 'Bad Light' is in a perpetually bad state and should always appear as red.
In the patio the scenes 'Light on' and 'Light off' turn the white light on and off. One or the other scene will be 'active' depending on what state the light is in.
Please don't try to change the configuration settings on this demo system since it is used by several developers. If something is messed up on it, email support [at] micasaverde [dot] com and someone will restore it back to the default setting.