├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── css ├── bootstrap.min.css ├── bootstrap.min.css.map └── style.css ├── index.html ├── index.js ├── js ├── bootstrap.min.js ├── bootstrap.min.js.map ├── d3.v5.min.js └── jquery-3.3.1.min.js ├── lib ├── CustomTypes.js ├── DailySocket.js ├── Group.js ├── Platform.js ├── Sensor.js ├── Service.js ├── ble.js ├── bleDailySwitch.js ├── helpers.js ├── iCal.js ├── mymath.js └── web.js ├── package-lock.json ├── package.json ├── support ├── index.png └── info.png ├── template.html └── testenv ├── sampleConfig.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | arduino.json 3 | c_cpp_properties.json 4 | backup 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support/ 2 | testenv/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Frank 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebridge-daily-sensors 2 | This programable switch can be triggered based on userdefine rules related to time, daily events, calendar events, sun elevation 3 | or amount of ambient light. Each TriggerEvent can be randomized to make sure that the rules trigger at slightly different times each day. 4 | 5 | ## Simple Configuration 6 | The following configuration is based on the suns elevation (altitude) in London. If the sun rises above 3° the switch will be triggered (with a single press (<= `active:true`)). If the sun gets below 3° it triggers a double press 7 | ```json 8 | "accessories": [{ 9 | "accessory": "DailySensors", 10 | "name":"My new daily switch", 11 | "dayStartsActive":false, 12 | "trigger":[{ 13 | "active":true, 14 | "type":"altitude", 15 | "value":"0.03", 16 | "trigger":"both" 17 | }], 18 | "location":{ 19 | "longitude":-0.118092, 20 | "latitude":51.509865 21 | } 22 | }] 23 | ``` 24 | 25 | ## How does it work? 26 | The plugin calculates typical sun events as well as the suns elevation based on a given location. Each sensor has a given activation state at any given time of a day. Whenever the activation state changes, the Programmable switch is notified. If the state changes from **off to on**, a **single press** is sent. if it changes from **on to off** a **double press** is generated. The activation state is determined through an array of TriggerEvents. 27 | 28 | All TriggerEvent are stored in the `trigger:array` config variable. Each TriggerEvent has the following basic properties: 29 | - `type` : The type of the TriggerEvent (`time`, `altitude`, `lux` `calendar`, `holiday`, `expression` or `event`). 30 | - `value` : A given treshold that is compared 31 | - `active` : The new activation state of the sensor if the TriggerEvent is triggered 32 | 33 | The sensor receives a tick event in a certain interval (config variable `tickTimer:seconds`). At every tick, all TriggerEvents are evaluated in sequence and the state of the sensor is determined (active/deactive) like this:s 34 | - First the activation state is set to the state defined in `dayStartsActive:bool`. 35 | - Each TriggerEvent (config variable `trigger:array`) is evaluated in the given sequence. 36 | - If a TriggerEvent is triggered, the activation state is recalculated. Normaly a TriggerEvent is triggered when the current value (like the current time) is greater than the specified `value` of the TriggerEvent and the activation state is set as was defined in the TriggerEvents' `active:bool` config variable. Take for exampl ethe following TriggerEvents: 37 | ```json 38 | "dayStartsActive":false, 39 | "trigger":[{ 40 | "active":true, 41 | "type":"time", 42 | "value":"10:00 AM" 43 | },{ 44 | "active":false, 45 | "type":"time", 46 | "value":"1:00 PM" 47 | }] 48 | ``` 49 | 50 | Evaluating the sensor at `11:00 PM` yields the following sequence: The evaluation starts with the activation state set to `false` (`"dayStartsActive":false`). Now the first TriggerEvent is calculated. Since `11:00 AM` (the current time) is larger than `10:00 AM` (the `"value":"10:00 AM"` of the TriggerEvent), the event is triggered and sets the activation state to `true`. The second TriggerEvent does not trigger as its value (`1:00 PM`) is less than the current time. The resulting activation state for `11:00 PM` is `true`. 51 | 52 | Evaluating the same set of TriggerEvents at `2:00 PM`generates a different activation state. As above, the evaluation starts with the activation state set to `false`. The first TriggerEvent will trigger as before and changes the activation state to `true`. However, the second TriggerEvent will also trigger and finally change the activation state back to `false`. Hence, the resulting activation state for `11:00 PM` is `true`. 53 | 54 | In this case the switch will be notified twice a day: 55 | - At `10:00 AM` when the activation state first changes from `false` to `true`resulting in a **single press** 56 | - At `1:00 PM` when the activation state changes back to `false`resulting in a **double press** 57 | 58 | ## Calendar Connection 59 | You can connect a caldav ready calendar (like iCloud, see https://community.openhab.org/t/solved-apple-icloud-caldav-connection/32510/6 to find the url, username and password to access an iCloud calendar) and use calendar events as triggers. The following config file will connect to the iCloud-calendar **https://pxx-caldav.icloud.com/xxxxxx/calendars/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/** with the user name **someone@me.com** and the application password **application-password**: 60 | 61 | ```json 62 | "accessories": [{ 63 | "accessory": "DailySensors", 64 | "name":"TheDaily", 65 | "port":7755, 66 | "dayStartsActive":false, 67 | "calendar":{ 68 | "url":"https://pxx-caldav.icloud.com/xxxxxx/calendars/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/", 69 | "username":"someone@me.com", 70 | "password":"application-password", 71 | "type":"caldav" 72 | }, 73 | "trigger":[{ 74 | "active":true, 75 | "type":"calendar", 76 | "value":"^Urlaub$", 77 | "trigger":"both" 78 | }], 79 | "location":{ 80 | "longitude":-0.118092, 81 | "latitude":51.509865 82 | } 83 | }] 84 | ``` 85 | 86 | The activation state changes to `true` whenever an Event named **Urlaub** starts and to `false` whenever it ends. 87 | 88 | ## Web Service 89 | The plugin offers a very simple web interface to force trigger the switch, check and reset the state as well as visualize the activation state of the switch over the day. This is a helpfull tool when debuging a given sequence of TriggerEvents. If you specify a config variable `port`, a web server will be started and bound. The index page (see below) will display an overview of available Accesories. 90 | 91 | ### Configuration 92 | For example, when using the follwoing config 93 | ```json 94 | "accessories": [{ 95 | "accessory": "DailySensors", 96 | "name":"TheDaily", 97 | "port":7755, 98 | "dayStartsActive":false, 99 | "trigger":[{ 100 | "active":true, 101 | "type":"altitude", 102 | "value":"0.03", 103 | "trigger":"both" 104 | }], 105 | "location":{ 106 | "longitude":-0.118092, 107 | "latitude":51.509865 108 | } 109 | }] 110 | ``` 111 | you may access the web interface through http://[homebridge-ip]:7755/thedaily/. Note that the lowercase name of the accessory is part of the URI. If your name contains non ASCII characters, you may want to specify the path for URL using the `webPath` variable. Having a config like this: 112 | 113 | ```json 114 | "accessories": [{ 115 | "accessory": "DailySensors", 116 | "name":"My very special Sensor Name", 117 | "webPath":"/thedaily/me", 118 | "port":7755, 119 | //... 120 | ``` 121 | will for example start the webservice on the base URL http://[homebridge-ip]:7755/thedaily/me/. Also note that you can have multiple DailySensors on the same port. 122 | 123 | ### Index Page 124 | To receive an overview of the available sensors on a given port you can open http://[homebridge-ip]:7755/. For a setup with tow configured accesories on the same port this will yield the following result: 125 | 126 | ![The Main Index Screen](/support/index.png) 127 | 128 | ### Visualize TriggerEvents 129 | If you open the root resource of an accesory (http://[homebridge-ip]:7755/thedaily/) the system will display the activation state of the sensor over the course of the entire day as well as display the results of the Evaluation steps for every minute of the day. 130 | 131 | ![The Activation Info Screen](/support/info.png) 132 | 133 | ### Force a state change 134 | If you access http://[homebridge-ip]:7755/thedaily/1 (or http://[homebridge-ip]:7755/thedaily/0) the switch is triggered with an **on** (**off**) event resulting in a **single press** (**double press**) notification. This locks the state of the switch to the given value until a activation state changes based on the defined rules. 135 | 136 | ### Clear forced state 137 | If you force a state change as described above, you can restore the normal operation of the switch using http://[homebridge-ip]:7755/thedaily/clear 138 | 139 | ### Query state 140 | You can also query the current state of the switch using http://[homebridge-ip]:7755/thedaily/state 141 | 142 | ### Force reload 143 | Some information is updated once a day (like calendar events). You can force a update of those events using http://[homebridge-ip]:7755/thedaily/reload. 144 | 145 | 146 | ## Config Options 147 | There are some additional config variables available: 148 | - `tickTimer:milliSeconds`: The interval at wich the Activation State is evaluated (defaults to 30s, `"tickTimer":30000`). 149 | - `debug:bool`: Wether you want to log some additional debug information (warning: this will generate a lot of output, defaults to false, `"debug":false`) 150 | - `locale:string`: The local used to parse date/time related values (like weekdays, defaults to english, `"locale":"en"`) 151 | 152 | ### Regional Holidays 153 | When you want to addres holidays in your expressions, you need to configure the lecation where you want to look up the holidays. You need to at least specify a country, all other values are optional: 154 | - `location.country:string` 155 | - `location.state:string` 156 | - `location.town:string` 157 | See [date-holiday](https://www.npmjs.com/package/date-holidays#supported-countries-states-regions) for possibal region codes. 158 | For a Sensor in **Melbourne** that enables the switch when the current day is a holiday you would use the following Configuration: 159 | ```json 160 | "accessories": [{ 161 | "accessory": "DailySensors", 162 | "name":"Holiday", 163 | "port":7755, 164 | "dayStartsActive":false, 165 | "trigger":[{ 166 | "type":"expression", 167 | "value":"now.isHoliday()", 168 | }], 169 | "location":{ 170 | "longitude":144.96332, 171 | "latitude":-37.814, 172 | "country":"AU", 173 | "state":"VIC", 174 | "town":"M" 175 | } 176 | }] 177 | ``` 178 | 179 | 180 | ### Advanced TriggerEvents 181 | Each TriggerEvent can have additional settings that alter its behaviour. 182 | 183 | #### Randomize 184 | Some TriggerEvent-Types can be randomized using the `random` setting. When this value is non zero, it is used to alter the given TriggerEvent value every day. When using `random` in a time based trigger, you can specify the amount of minutes added (or subtracted) at max from the value every day. The following example will generate times from **6:50 am** to **7:10 am**: 185 | ```json 186 | "trigger":[{ 187 | "active":true, 188 | "type":"time", 189 | "value":"7:00 AM", 190 | "random":10 191 | }] 192 | ``` 193 | 194 | See the type descriptions below for additional information on the behaviour of random for every type. 195 | 196 | #### Operations 197 | The operation controls how the activation state of the triggered TriggerEvent is applied to the result of the previous TriggerEvent. The behaviour is configured using the `op`-value. We will use the following trigger sequence as an example in the descriptions below. 198 | ```json 199 | "dayStartsActive":false, 200 | "trigger":[{ 201 | "active":true, 202 | "type":"time", 203 | "value":"10:00 AM" 204 | }] 205 | ``` 206 | 207 | The following values are recognized: 208 | - `set` : The default behaviour. When the event is triggered the activation state is set to the specified value. 209 | - `and` : A logical **and** is used to calculate the new activation state. Evaluating the sensor at `11:00 PM` yields the following sequence: The evaluation starts with the activation state set to `false` (`"dayStartsActive":false`). Now the first TriggerEvent is calculated. Since `11:00 AM` (the current time) is larger than `10:00 AM` (the `"value":"10:00 AM"` of the TriggerEvent), the event is triggered. Since `false` (the current state) **and** `true` (the value of the TriggerEvent) results in `false` the activation state remains `false`. 210 | - `or` : A logical **or** is used to calculate the new activation state. Evaluating the sensor at `11:00 PM` yields the following sequence: The evaluation starts with the activation state set to `false` (`"dayStartsActive":false`). Now the first TriggerEvent is calculated. Since `11:00 AM` (the current time) is larger than `10:00 AM` (the `"value":"10:00 AM"` of the TriggerEvent), the event is triggered. Since `false` (the current state) **or** `true` (the value of the TriggerEvent) results in `true` the activation state is change to `true`. 211 | - `discard` : The TriggerEvent is ignored. 212 | 213 | #### Trigger Condition 214 | By default an EventTrigger is triggered when the current value (like the current time) is greater than the specified value. Only a triggered event can alter the activation state. Using the `trigger` parameter, allows you to specify a different behaviour. The following values are possible: 215 | 216 | - `greater` : The default behaviour. 217 | - `less` : The Event is triggered when the current value is less than the specified one. When the event triggers before the actual value, the `active`-value is negated. The following Event will only trigger before **2:00 pm** and will set the activation state to `false` (since `"active":true` is negated): 218 | ```json 219 | "trigger":[{ 220 | "active":true, 221 | "type":"time", 222 | "value":"2:00 PM", 223 | "trigger":"less" 224 | }] 225 | ``` 226 | - `both` : The event is allways triggered. When the event triggers before the actual value, the `active`-value is negated. When the following Event is triggered **before 2:00 pm** the activation state changes to `false` when it is triggered **after 2:00 pm** the activation state changes to `true`. 227 | ```json 228 | "trigger":[{ 229 | "active":true, 230 | "type":"time", 231 | "value":"2:00 PM", 232 | "trigger":"both" 233 | }] 234 | ``` 235 | 236 | #### Days Of Week 237 | The `daysOfWeek`-value contains an array of weekdays (in the specified locale). The event can only trigger on days listed in this array. For example, the following TriggerEvent is only triggered on **Weekends** after **10:00 am**. 238 | ```json 239 | "trigger":[{ 240 | "active":true, 241 | "type":"time", 242 | "value":"10:00 AM", 243 | "daysOfWeek":["sa", "so"] 244 | }] 245 | ``` 246 | 247 | #### Types 248 | The following settings are available for the given TriggerEvent-Types. 249 | 250 | ##### Time 251 | - `type` : `time` 252 | - `value` : 253 | - `random` : 254 | 255 | ##### Altitude 256 | - `type` : `altitude` 257 | - `value` : 258 | - `random` : 259 | 260 | ##### Lux 261 | - `type` : `lux` 262 | - `value` : 263 | - `random` : 264 | 265 | ##### Event 266 | - `type` : `event` 267 | - `value` : one of the following event types `nightEnd`, `nauticalDawn`, `dawn`, `sunrise`, `sunriseEnd`, `goldenHourEnd`, `solarNoon`, `goldenHour`, `sunsetStart`, `sunset`, `dusk`, `nauticalDusk`, `night`, `nadir` 268 | - `trigger` : Notwithstanding the default behaviour the following settings are allowed for this property: 269 | - `match` : The Trigger Event is triggered when the specified event is currently active. The activation state is set to the value specified in the `active`-property. 270 | - `no-match` : The Trigger Event is triggered when the specified event is **not** currently active. The activation state is set to the **inverse** value specified in the `active`-property. 271 | - `always` : The Trigger Event is allways triggered. If the pecified event is currently active, the activation state is set to the value specified in the `active`-property. Otherwise it is set to the inverse. 272 | 273 | ##### Calendar Event 274 | - `type` : `calendar` 275 | - `value` : A JavaScript RegExp. `Hello` will match any Event that contains the Word **Hello** (ie. 'Hello World', 'My Beautiful Hello', 'Hello'). `^Hello$` will only match Events where the summary is the Word **Hello** ('Hello World' and 'My Beautiful Hello' do not match). 276 | - `trigger` : Notwithstanding the default behaviour the following settings are allowed for this property: 277 | - `match` : The Trigger Event is triggered when at least one calendar event was found for the current time and the summary matches the specified regexp . The activation state is set to the value specified in the `active`-property. 278 | - `no-match` : The Trigger Event is triggered when no valid calendar event was found. The activation state is set to the **inverse** value specified in the `active`-property. 279 | - `always` : The Trigger Event is allways triggered. If a valid calendar event was found, the activation state is set to the value specified in the `active`-property. Otherwise it is set to the inverse. 280 | 281 | 282 | ##### Holiday 283 | In order to use holiday triggers, you need to configure your Region. See **Regional Holidays** above for details. The event triggers when the current day is a holiday and the type of the holiday is found in the array passed as `value`-property. See [date-holiday](https://www.npmjs.com/package/date-holidays#types-of-holidays) for more info on available holiday types. 284 | - `type` : `holiday` 285 | - `value` : An array containing a list of valid holiday types. Default is `value:["public", "bank"]`. 286 | - `trigger` : Notwithstanding the default behaviour the following settings are allowed for this property: 287 | - `match` : The Trigger Event is triggered when the day is a valid holiday. The activation state is set to the value specified in the `active`-property. 288 | - `no-match` : The Trigger Event is triggered when the day is **not** a valid holiday. The activation state is set to the **inverse** value specified in the `active`-property. 289 | - `always` : The Trigger Event is allways triggered. If the day is a valid holiday, the activation state is set to the value specified in the `active`-property. Otherwise it is set to the inverse. 290 | 291 | ##### Expression 292 | This type allows you to write a logical expression to determinthe activation state. The underlying parser is [Math.js](http://mathjs.org/docs/core/extension.html) with a few custom extensions to handle some time and calendar based events. 293 | 294 | - `type` : `expression` 295 | - `constants` : List of constants you can use in your expression. You can add time values with a String like `Time(15:30)`. For example: 296 | ```json 297 | "constants":{ 298 | "tm":"Time(6:30)", 299 | "nm":"Vacation", 300 | "nr":42 301 | } 302 | ``` 303 | 304 | - `value` : An logical expresion. You may use all functions available in [Math.js](http://mathjs.org/docs/core/extension.html), as well as the following expressions 305 | - Functions 306 | - `dailyrandom(nr, magnitude)` : creates a random value with index `nr` that is maintained for the entire day. You can use this to generate multiple random numbers for your expressions that do not change with each evaluation of the expression on the same day. The random value is generated between 0 (included) and `magnitude` (excluded). 307 | - `dailyrandom(nr)` : same as `dailyrandom(nr, 1)` 308 | - `dailyrandom()` : same as `dailyrandom(0, 1)` 309 | - `Time("str")` : creates a date-object. Valid String Formats are `h:m a`, `H:m`, `h:m:s a`, `H:m:s`, `YYYY-MM-DD H:m:s`, `YYYY-MM-DD H:m`,`YYYY-MM-DD`, `YYYY-MM-DD h:m:s a`, `YYYY-MM-DD h:m a`. Time Values can be compared using `<`, `>` and `==`. Two date Values are equal if they match up to the second. In the following `t` represents a Time-constant. 310 | - `t.mo()` : `true` if the represented date is a monday. 311 | - `t.tu()` : `true` if the represented date is a tuesday. 312 | - `t.we()` : `true` if the represented date is a wednesday. 313 | - `t.th()` : `true` if the represented date is a thursday. 314 | - `t.fr()` : `true` if the represented date is a friday. 315 | - `t.sa()` : `true` if the represented date is a saturday. 316 | - `t.so()` : `true` if the represented date is a sunday. 317 | - `t.workday()` : `true` if the represented date is a mo through fr. 318 | - `t.weekend()` : `true` if the represented date is sa or so. 319 | - `t.weekday()` : returns a number that represents a ISO-weekday (mo=1, tu=2, ...) 320 | - `t.time()` : returns just the time of the date 321 | - `t.addMinutes(nr)` : adds `nr`-minutes to `t` and returns the new date. 322 | - `t.isHoliday([types])` : returns true if the date is a holiday **and** the type of that holiday (see [date-holiday](https://www.npmjs.com/package/date-holidays#types-of-holidays) ) is found in the passed types-Array. 323 | - `t.isHoliday()` : Same as `t.isHoliday(['public', 'bank'])`. 324 | - `t.calendarMatch(regex)` : true if the linked calendar has an event for the date `t` that matches the passed regexp. 325 | - `t.isEvent(name)` : true if the passed solar event is active at the given time. See the **Event**-Type for possible values. 326 | - Constants 327 | - `altitude` : The current elevation of the sun in radians 328 | - `altitudeDeg` : The current elevation of the sun in degrees 329 | - `azimuth` : The suns current azimuth in radians 330 | - `azimuthDeg` : The suns current azimuth in degrees 331 | - `lux` : The current brightness 332 | - `now` : The current time and date 333 | - `self` : References the sensor that is executing the expression 334 | 335 | 336 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin : 20px; 3 | } 4 | 5 | templates { 6 | display:none; 7 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Daily Sensors Overview 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | 30 | 36 |
37 |
38 |
39 |
Activation State:
40 | 41 | 42 | 43 |
44 |
45 |
46 |
47 |
48 |
Activation State:
49 |

50 | Homebridge State: 51 |

52 | Forced:
53 | Actual:
54 |
55 | 56 | 57 | 58 | 59 | 60 |

61 | 62 |
63 | 67 |
68 |
69 | 70 |
71 |

Daily Sensors ({{VERSION}})

72 | 73 |
74 |
75 |
76 |
77 | 78 | 212 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const web = require('./lib/web.js'), 3 | $ = require('./lib/helpers.js'); 4 | 5 | var Service, Characteristic, Accessory, UUIDGen; 6 | 7 | module.exports = function(homebridge) { 8 | console.log("homebridge API version: " + homebridge.version); 9 | console.logEvents = $.logEvents; 10 | 11 | // Accessory must be created from PlatformAccessory Constructor 12 | Accessory = homebridge.platformAccessory; 13 | 14 | // Service and Characteristic are from hap-nodejs 15 | Service = homebridge.hap.Service; 16 | Characteristic = homebridge.hap.Characteristic; 17 | UUIDGen = homebridge.hap.uuid; 18 | 19 | web.setCharacteristics(Characteristic); 20 | 21 | const CustomTypes = require('./lib/CustomTypes.js')(Service, Characteristic, UUIDGen); 22 | const DailySensor = require('./lib/Sensor.js')(Service, Characteristic).DailySensor; 23 | const DailyGroup = require('./lib/Group.js')(Service, Characteristic, DailySensor).DailyGroup; 24 | const DailySocket = require('./lib/DailySocket.js')(Service, Characteristic, CustomTypes); 25 | const Services = require('./lib/Service.js')(Service, Characteristic, Accessory, UUIDGen, DailySensor, DailyGroup, DailySocket.DailySocket, DailySocket.DailyLight); 26 | const DailyPlatform = require('./lib/Platform.js')(Service, Characteristic, Accessory, UUIDGen, Services, DailySensor, DailyGroup, DailySocket.DailySocket, DailySocket.DailyLight).DailyPlatform; 27 | 28 | //console.log(DailySensor, DailyGroup) 29 | homebridge.registerAccessory("homebridge-daily-sensors", "DailySensors", DailySensor); 30 | homebridge.registerAccessory("homebridge-daily-sensors", "DailyGroup", DailyGroup); 31 | homebridge.registerAccessory("homebridge-daily-sensors", "DailySocket", DailySocket.DailySocket); 32 | homebridge.registerAccessory("homebridge-daily-sensors", "DailyLight", DailySocket.DailyLight); 33 | homebridge.registerPlatform("homebridge-daily-sensors", "DailyPlatform", DailyPlatform, true); 34 | } 35 | -------------------------------------------------------------------------------- /lib/CustomTypes.js: -------------------------------------------------------------------------------- 1 | const inherits = require('util').inherits 2 | let CustomTypes = undefined; 3 | 4 | module.exports = function(Service, Characteristic, UUID){ 5 | if (CustomTypes===undefined){ 6 | const modeUUID = UUID.generate('CustomTypes:usagedevice:LightMode'); 7 | const labelUUID = UUID.generate('CustomTypes:usagedevice:LightModeLabel'); 8 | 9 | class LightMode extends Characteristic { 10 | constructor() { 11 | super('Light Mode', modeUUID); 12 | 13 | this.setProps({ 14 | format: Characteristic.Formats.UINT8, 15 | maxValue: 16, 16 | minValue: 0, 17 | minStep: 1, 18 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 19 | }); 20 | this.value = this.getDefaultValue(); 21 | } 22 | }; 23 | LightMode.UUID = modeUUID; 24 | 25 | class LightModeLabel extends Characteristic { 26 | constructor() { 27 | super('Light Mode Label', labelUUID); 28 | 29 | this.setProps({ 30 | format: Characteristic.Formats.String, 31 | perms: [ Characteristic.Perms.READ, Characteristic.Perms.NOTIFY ] 32 | }); 33 | this.value = this.getDefaultValue(); 34 | } 35 | }; 36 | LightModeLabel.UUID = labelUUID; 37 | CustomTypes = {LightMode:LightMode, LightModeLabel:LightModeLabel}; 38 | } 39 | 40 | return CustomTypes; 41 | } -------------------------------------------------------------------------------- /lib/DailySocket.js: -------------------------------------------------------------------------------- 1 | const packageJSON = require("../package.json"), 2 | path = require('path'), 3 | web = require('./web.js'), 4 | axios = require('axios'), 5 | qs = require('querystring'), 6 | $ = require('./helpers.js'); 7 | 8 | let Service, Characteristic, CustomTypes; 9 | 10 | 11 | 12 | class DailySocket { 13 | constructor(log, config, api, owner, modeLight) { 14 | if (modeLight === undefined) modeLight = false; 15 | this.modeLight = modeLight; 16 | 17 | this.debug = config.debug === undefined ? false : config.debug; 18 | if (log.debug !== undefined) this.log = log; 19 | else this.log = {info:log, debug:this.debug?console.log:function(){}, error:console.error}; 20 | if (config.debug !== undefined) 21 | this.log.debug = this.debug?this.log.info:function(){}; 22 | 23 | 24 | this._setConfig(config); 25 | this.port = this.config.port ? this.config.port : 0; 26 | this.webPath = path.resolve('/', './' + (this.config.webPath ? this.config.webPath : this.config.name.toLowerCase()) + '/'); 27 | 28 | this.log.info("Initializing", this.name, this.modeLight); 29 | 30 | this.comType = $.CommunicationTypes.http; 31 | this.mode = 1; 32 | this.speed = 20; 33 | this.brightness = 100; 34 | this.hue = 0; 35 | this.saturation = 100; 36 | 37 | // On: Bool 38 | this.On = false; 39 | 40 | this.service = undefined; 41 | web.startServerForSocket(this); 42 | } 43 | 44 | webCallback(json){ 45 | this.log.debug("Callback", json); 46 | if (this.service){ 47 | if (json.on !== undefined){ 48 | this.updateOnState(json.on); 49 | } 50 | if (this.modeLight){ 51 | this.updateBrightness(Math.round((json.v / 0xFF) * 100)); 52 | this.updateSaturation(Math.round((json.s / 0xFF) * 100)); 53 | this.updateHue(Math.round((json.h / 0xFF) * 100)); 54 | 55 | this.updateMode(json.mode); 56 | this.updateSpeed(json.speed); 57 | } 58 | } 59 | return true; 60 | } 61 | 62 | updateOnState(value){ 63 | if (value != this.On) { 64 | this.On = value; 65 | this.log.debug("Updating On State:", value) 66 | this.service 67 | .getCharacteristic(Characteristic.On) 68 | .updateValue(value); 69 | } 70 | } 71 | 72 | updateBrightness(value){ 73 | if (value != this.brightness && this.modeLight) { 74 | this.brightness = value; 75 | this.log.debug("Updating Brightness:", value) 76 | this.service 77 | .getCharacteristic(Characteristic.Brightness) 78 | .updateValue(value); 79 | } 80 | } 81 | 82 | updateHue(value){ 83 | if (value != this.hue && this.modeLight) { 84 | this.hue = value; 85 | this.log.debug("Updating Hue:", value) 86 | this.service 87 | .getCharacteristic(Characteristic.Hue) 88 | .updateValue(value); 89 | } 90 | } 91 | 92 | updateSaturation(value){ 93 | if (value != this.saturation && this.modeLight) { 94 | this.saturation = value; 95 | this.log.debug("Updating Saturation:", value) 96 | this.service 97 | .getCharacteristic(Characteristic.Saturation) 98 | .updateValue(value); 99 | } 100 | } 101 | 102 | updateMode(val){ 103 | if (val != this.mode && this.modeLight) { 104 | this.mode = val 105 | this.log.debug("Updating Mode:", val); 106 | 107 | this.service 108 | .getCharacteristic(CustomTypes.LightMode) 109 | .updateValue(val); 110 | if (val>=0 && val { 156 | //console.log("TEST", err, json) 157 | if (err){ 158 | this.log.error(err); 159 | this.service 160 | .getCharacteristic(Characteristic.On). 161 | 162 | return 163 | } else { 164 | if (json.on !== undefined){ 165 | this.updateOnState(json.on); 166 | } 167 | if (this.modeLight){ 168 | this.updateBrightness(json.v); 169 | this.updateSaturation(json.s); 170 | this.updateHue(json.h); 171 | 172 | this.updateMode(json.mode); 173 | this.updateSpeed(json.speed); 174 | } 175 | } 176 | }) 177 | } 178 | 179 | retries = [] 180 | retryGetFromOutlet(host, callback, retryCount){ 181 | const self = this 182 | if (self.retries == undefined) self.retries = [] 183 | 184 | //console.log("T1", self.retries) 185 | if (self.retries.indexOf(host.uris.state)>=0){ 186 | return; 187 | } 188 | self.retries.push(host.uris.state) 189 | const retryIn = retryCount>10 ? 300 : 2; 190 | 191 | if (retryCount ===undefined) retryCount = 0 192 | self.log.error(` Will retry in ${retryIn}s`) 193 | //console.log("T2", self.retries) 194 | setTimeout(()=>{ 195 | self.log.info(`Retrying to receive data from '${self.config.name}' (${host.uris.state})`) 196 | self.retries = self.retries.filter(u => u!==host.uris.state) 197 | //console.log("T3", self.retries) 198 | self.getFromOutlet(callback, retryCount+1) 199 | }, retryIn*1000) 200 | } 201 | 202 | getFromOutlet(callback, retryCount){ 203 | 204 | const self = this; 205 | axios.get(this.host.uris.state) 206 | .then(function (response) { 207 | self.log.debug("getFromOutlet: ", response.data, response.status, response.statusText); 208 | if (response.status == 200) { 209 | callback(null, response.data[0]); 210 | } else { 211 | self.log.error(`Status ${response.status}: ${response.statusText}.`) 212 | callback(`Status ${response.status}: ${response.statusText}`, null); 213 | self.retryGetFromOutlet(self.host, callback, retryCount) 214 | } 215 | 216 | }) 217 | .catch(function (error) { 218 | callback("Failed to Connect", null); 219 | self.log.error("getFromOutlet failed ", error.syscall, error.address, error.code); 220 | self.retryGetFromOutlet(self.host, callback, retryCount) 221 | }); 222 | } 223 | 224 | sendToOutlet(value, callback){ 225 | const self = this; 226 | console.log(value?this.host.uris.on:this.host.uris.off); 227 | axios.get(value?this.host.uris.on:this.host.uris.off) 228 | .then(function (response) { 229 | self.log.debug("sendToOutlet: ", value, response.data, response.status, response.statusText); 230 | if (response.status == 200) { 231 | callback(null); 232 | } else { 233 | callback(`Status ${response.status}: ${response.statusText}`); 234 | } 235 | 236 | }) 237 | .catch(function (error) { 238 | callback(error); 239 | self.log.error("sendToOutlet failed ", error.syscall, error.address, error.code); 240 | }); 241 | } 242 | 243 | sendModeOutlet(callback){ 244 | if (this.sendModeOutletTimer!==undefined){ 245 | clearTimeout(this.sendModeOutletTimer.timeout); 246 | //this.sendModeOutletTimer.callback(null); 247 | this.sendModeOutletTimer = undefined; 248 | 249 | } 250 | 251 | this.sendModeOutletTimer = { 252 | timeout: setTimeout(this._sendModeOutlet.bind(this, function(c){}), 100), 253 | callback: callback 254 | } 255 | callback(null); 256 | } 257 | 258 | _sendModeOutlet(callback){ 259 | this.sendColorOutletTimer = undefined; 260 | const self = this; 261 | const mode = { 262 | mode:this.mode, 263 | speed: Math.max(0.01, ((this.speed/100)*this.maxSpeed)) 264 | }; 265 | self.log.debug("Sending Mode", mode); 266 | axios.post(this.host.uris.mode, qs.stringify(mode), { 267 | headers: { 268 | 'Content-Type': 'application/x-www-form-urlencoded' 269 | } 270 | }) 271 | .then(function (response) { 272 | self.log.debug("sendModeOutlet: ", response.data, response.status, response.statusText); 273 | if (response.status == 200) { 274 | callback(null); 275 | } else { 276 | callback(`Status ${response.status}: ${response.statusText}`); 277 | } 278 | 279 | }) 280 | .catch(function (error) { 281 | callback(error); 282 | self.log.error("sendModeOutlet: ", error); 283 | }); 284 | } 285 | 286 | sendColorOutlet(callback){ 287 | if (this.sendColorOutletTimer!==undefined){ 288 | clearTimeout(this.sendColorOutletTimer.timeout); 289 | //this.sendColorOutletTimer.callback(null); 290 | this.sendColorOutletTimer = undefined; 291 | 292 | } 293 | 294 | this.sendColorOutletTimer = { 295 | timeout: setTimeout(this._sendColorOutlet.bind(this, function(c){}), 100), 296 | callback: callback 297 | } 298 | callback(null); 299 | } 300 | 301 | _sendColorOutlet(callback){ 302 | this.sendColorOutletTimer = undefined; 303 | const self = this; 304 | const colors = { 305 | h:Math.round((this.hue / 360) * 0xFF), 306 | s:Math.round((this.saturation / 100) * 0xFF), 307 | v:Math.max(32, Math.round((this.brightness / 100) * 0xFF)) 308 | }; 309 | self.log.debug("Sending Colors", colors); 310 | axios.post(this.host.uris.color, qs.stringify(colors), { 311 | headers: { 312 | 'Content-Type': 'application/x-www-form-urlencoded' 313 | } 314 | }) 315 | .then(function (response) { 316 | self.log.debug("sendColorOutlet: ", response.data, response.status, response.statusText); 317 | if (response.status == 200) { 318 | callback(null); 319 | } else { 320 | callback(`Status ${response.status}: ${response.statusText}`); 321 | } 322 | 323 | }) 324 | .catch(function (error) { 325 | callback(error); 326 | self.log.error("sendColorOutlet: ", error); 327 | }); 328 | } 329 | 330 | // Required 331 | getInUse (callback) { 332 | this.log.debug("getInUse :", this.On); 333 | callback(null, true); 334 | } 335 | 336 | getOn (callback) { 337 | this.getFromOutlet(function (error, powerOn) { 338 | if (error) { 339 | //callback(error, this.On); 340 | } else { 341 | this.On = powerOn.on; 342 | this.log.debug("getOn :", this.On); 343 | 344 | //callback(null, this.On); 345 | this.updateOnState(this.On); 346 | } 347 | }.bind(this)); 348 | if (this.On === null) { 349 | this.On = false; 350 | } 351 | callback(null, this.On); 352 | } 353 | 354 | setOn (value, callback) { 355 | if (value === undefined) { 356 | callback(null); 357 | } else { 358 | this.log.debug("setOn from/to:", this.On, value); 359 | var oldState = this.On; 360 | this.On = value; 361 | //if ((value == false) || (this.bChangeSth == false)) 362 | { 363 | this.sendToOutlet(value, function (error) { 364 | if (error) { 365 | this.On = oldState; 366 | callback(error); 367 | } else { 368 | callback(null); 369 | } 370 | }.bind(this)); 371 | } 372 | // else { 373 | // callback(null); 374 | // } 375 | } 376 | } 377 | 378 | getName(callback) { 379 | this.log.debug("getName :", this.name); 380 | callback(null, this.name); 381 | } 382 | 383 | getMode (callback) { 384 | this.getFromOutlet(function (error, json) { 385 | if (error) { 386 | //callback(error, this.mode); 387 | } else { 388 | this.mode = json.mode; 389 | this.log.debug("getMode :", this.mode); 390 | //callback(null, this.mode); 391 | this.updateMode(this.mode); 392 | } 393 | }.bind(this)); 394 | callback(null, this.mode); 395 | } 396 | 397 | setMode(val, callback){ 398 | this.log.debug("setMode from/to", this.mode, val); 399 | let self = this; 400 | this.mode = val; 401 | if (val>=0 && val=0 && val0){ 527 | if (lightModeC === undefined) lightModeC = this.service.addCharacteristic(CustomTypes.LightMode); 528 | if (lightModeLabelC === undefined) lightModeLabelC = this.service.addCharacteristic(CustomTypes.LightModeLabel); 529 | if (lightModeSpeed === undefined) lightModeSpeed = this.service.addCharacteristic(Characteristic.RotationSpeed); 530 | 531 | this.log.debug(`Configuring for ${this.modes.length} modes.`) 532 | lightModeC.setProps({ 533 | format: Characteristic.Formats.UINT8, 534 | maxValue: this.modes.length-1, 535 | minValue: 0, 536 | minStep: 1, 537 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 538 | }) 539 | 540 | lightModeC 541 | .on('get', this.getMode.bind(this)) 542 | .on('set', this.setMode.bind(this)); 543 | 544 | lightModeLabelC 545 | .on('get', this.getModeLabel.bind(this)); 546 | 547 | lightModeSpeed 548 | .on('get', this.getSpeed.bind(this)) 549 | .on('set', this.setSpeed.bind(this)); 550 | } else { 551 | if (lightModeC !== undefined) this.service.removeCharacteristic(CustomTypes.LightMode); 552 | if (lightModeLabelC !== undefined) this.service.removeCharacteristic(CustomTypes.LightModeLabel); 553 | if (lightModeSpeed !== undefined) this.service.removeCharacteristic(Characteristic.RotationSpeed); 554 | } 555 | 556 | this.service.getCharacteristic(Characteristic.Brightness) 557 | .on('get', this.getBrightness.bind(this)) 558 | .on('set', this.setBrightness.bind(this)); 559 | 560 | this.service.getCharacteristic(Characteristic.Hue) 561 | .on('get', this.getHue.bind(this)) 562 | .on('set', this.setHue.bind(this)); 563 | 564 | this.service.getCharacteristic(Characteristic.Saturation) 565 | .on('get', this.getSaturation.bind(this)) 566 | .on('set', this.setSaturation.bind(this)); 567 | } else { 568 | this.service 569 | .getCharacteristic(Characteristic.OutletInUse) 570 | .on('get', this.getInUse.bind(this)); 571 | } 572 | 573 | this.service 574 | .getCharacteristic(Characteristic.Name) 575 | .on('get', this.getName.bind(this)); 576 | } 577 | 578 | 579 | getServices () { 580 | if (this.service === undefined) { 581 | this.service = this.modeLight ? new Service.Lightbulb(this.name) : new Service.Outlet(this.name); 582 | 583 | this.service.addCharacteristic(Characteristic.Brightness); 584 | this.service.addCharacteristic(Characteristic.Hue); 585 | this.service.addCharacteristic(Characteristic.Saturation); 586 | } 587 | 588 | // you can OPTIONALLY create an information service if you wish to override 589 | // the default values for things like serial number, model, etc. 590 | const informationService = new Service.AccessoryInformation(); 591 | 592 | informationService 593 | .setCharacteristic(Characteristic.Manufacturer, "Ambertation") 594 | .setCharacteristic(Characteristic.Model, "AirSwitch") 595 | .setCharacteristic(Characteristic.SerialNumber, "0000"); 596 | 597 | this.configureServices(informationService, this.service); 598 | 599 | 600 | //Initialise for talking to the bulb 601 | this.initCommunications(); 602 | 603 | return [informationService, this.service]; 604 | } 605 | } 606 | 607 | class DailyLight extends DailySocket{ 608 | constructor(log, config, api, owner){ 609 | super(log, config, api, owner, true); 610 | this.log.info("Initializing Light", this.name); 611 | } 612 | } 613 | 614 | module.exports = function(service, characteristic, customTypes){ 615 | Service = service; 616 | Characteristic = characteristic; 617 | CustomTypes = customTypes; 618 | 619 | return { 620 | DailySocket:DailySocket, 621 | DailyLight:DailyLight 622 | } 623 | } -------------------------------------------------------------------------------- /lib/Group.js: -------------------------------------------------------------------------------- 1 | const suncalc = require('suncalc'), 2 | moment = require('moment'), 3 | packageJSON = require("../package.json"), 4 | path = require('path'), 5 | holidays = require('date-holidays'), //https://www.npmjs.com/package/date-holidays#supported-countries-states-regions 6 | ical = require('./iCal.js'), 7 | web = require('./web.js'), 8 | $ = require('./helpers.js'); 9 | 10 | let Service, Characteristic, DailySensor; 11 | 12 | 13 | class DailyGroup { 14 | constructor(log, config, api, owner) { 15 | this.owner = owner; 16 | const self = this; 17 | this.config = config === undefined ? {} : config; 18 | this.debug = this.config.debug === undefined ? false : this.config.debug; 19 | this.fixedConfig = true; 20 | this.items = []; 21 | this.subServices = []; 22 | this.port = this.config.port ? this.config.port : 0; 23 | this.webPath = path.resolve('/', './' + (this.config.webPath ? this.config.webPath : this.config.name.toLowerCase()) + '/'); 24 | 25 | if (log.debug !== undefined) this.log = log; 26 | else this.log = {info:log, debug:this.debug?log:function(){}, error:console.error}; 27 | if (this.config.debug !== undefined) 28 | this.log.debug = this.debug?this.log.info:function(){}; 29 | 30 | this._setConfig(config); 31 | if (api) { 32 | this.api = api; 33 | this.api.on('didFinishLaunching', function() { 34 | self.log.info("DidFinishLaunching", this.config.name); 35 | }.bind(this)); 36 | } 37 | 38 | web.startServerForGroup(this); 39 | this.log.debug("Finished Initialization"); 40 | } 41 | 42 | identify(callback) { 43 | this.log.info('Identify requested!'); 44 | callback(null); 45 | } 46 | 47 | receivedSwitchEvent(e){ 48 | this.log.debug("Broadcasting Event", e); 49 | this.items 50 | .filter(sensor => ( 51 | sensor.bluetooth && sensor.bluetooth.id === e.id 52 | )) 53 | .forEach(sensor => sensor.receivedSwitchEvent(e)); 54 | } 55 | 56 | setConfig(config){ 57 | this._setConfig(config); 58 | } 59 | 60 | _setConfig(config){ 61 | this.log.info("Updating Config for " + config.name); 62 | this.config = config; 63 | this.debug = this.config.debug || false; 64 | } 65 | 66 | configureAccesory(acc){ 67 | let infoService = acc.getService(Service.AccessoryInformation); 68 | let labelService = acc.getService(Service.ServiceLabel); 69 | this.configureServices(infoService, labelService); 70 | } 71 | 72 | configureServices(informationService, labelService){ 73 | const self = this; 74 | 75 | this.informationService = informationService; 76 | this.labelService = labelService; 77 | 78 | 79 | let counter = 0; 80 | this.log.info("Running Config"); 81 | this.config.items.forEach(subConf => { 82 | counter++; 83 | const name = subConf.name ? subConf.name : 'Item ' + counter; 84 | const type = subConf.accessory; 85 | if (type == 'DailySensors') { 86 | self.log.debug("Creating '"+name+"' as '"+type+"'"); 87 | subConf.noLux = true; 88 | subConf.noPowerState = true; 89 | subConf = { 90 | ...self.config, 91 | ...subConf 92 | }; 93 | const sensor = new DailySensor(self.log, subConf, self.api, self.owner); 94 | self.items.push(sensor); 95 | const services = sensor.getServices(informationService); 96 | 97 | services.forEach(service=>{ 98 | if (informationService.UUID != service.UUID) { 99 | const sli = service.getCharacteristic(Characteristic.ServiceLabelIndex); 100 | 101 | sli && sli.setValue(sensor.bluetooth.id ? sensor.bluetooth.id : counter); 102 | self.subServices.push(service); 103 | } 104 | }); 105 | } else { 106 | this.log.error("Unknown Type '"+type+"' for '"+name+"'"); 107 | } 108 | }); 109 | } 110 | 111 | configureServiceCharacteristics(informationService, labelService){ 112 | informationService 113 | .setCharacteristic(Characteristic.Manufacturer, "Ambertation") 114 | .setCharacteristic(Characteristic.Model, "Daily Group") 115 | .setCharacteristic(Characteristic.SerialNumber, "0000") 116 | .setCharacteristic(Characteristic.FirmwareRevision, packageJSON.version); 117 | 118 | this.configureServices(informationService, labelService) 119 | } 120 | 121 | getServices(useInfo) { 122 | if (this.informationService == undefined){ 123 | //Info about this plugin 124 | let informationService = useInfo; 125 | if (!useInfo) informationService = new Service.AccessoryInformation (); 126 | 127 | let labelService = new Service.ServiceLabel(this.name, '') 128 | labelService 129 | .getCharacteristic(Characteristic.ServiceLabelNamespace) 130 | .updateValue(Characteristic.ServiceLabelNamespace.ARABIC_NUMERALS); 131 | 132 | 133 | 134 | 135 | 136 | this.configureServiceCharacteristics(informationService, labelService); 137 | } 138 | 139 | let services = [this.informationService, this.labelService, ...this.subServices]; 140 | //console.log(services); 141 | return services; 142 | } 143 | } 144 | 145 | module.exports = function(service, characteristic, dailySensor){ 146 | Service = service; 147 | Characteristic = characteristic; 148 | DailySensor = dailySensor; 149 | 150 | return { 151 | DailyGroup:DailyGroup 152 | } 153 | } -------------------------------------------------------------------------------- /lib/Platform.js: -------------------------------------------------------------------------------- 1 | const packageJSON = require("../package.json"), 2 | path = require('path'), 3 | web = require('./web.js'), 4 | $ = require('./helpers.js') 5 | 6 | let Service, Characteristic, Accessory, UUIDGen, DailySensor, DailyGroup, DailySocket, DailyLight, Services; 7 | 8 | class DailyPlatform { 9 | services = [] 10 | 11 | constructor(log, config, api) { 12 | if (config.services && Array.isArray(config.services)){ 13 | this.services = this.services.concat(config.services.map(cfg => new Services.DailyService(log, cfg, api))) 14 | } else { 15 | this.services.push(new Services.DailyService(log, config, api)) 16 | } 17 | } 18 | 19 | configureAccessory(accessory) { 20 | for (let i = 0; i acc.displayName == name); 31 | } 32 | 33 | getAccessoryWithName(name) { 34 | return Services.this.accessories.find(acc => acc.config.name == name); 35 | } 36 | 37 | hasAccessoryWithName(name){ 38 | console.log("HAS", name, Services.this.accessories.length) 39 | return Services.this.accessories.some(acc => acc.config.displayName == name); 40 | } 41 | } 42 | 43 | module.exports = function(service, characteristic, accessory, uuidGen, services, dailySensor, dailyGroup, dailySocket, dailyLight){ 44 | Service = service; 45 | Characteristic = characteristic; 46 | Accessory = accessory; 47 | UUIDGen = uuidGen; 48 | DailySensor = dailySensor; 49 | DailyGroup = dailyGroup; 50 | DailySocket = dailySocket; 51 | DailyLight = dailyLight; 52 | Services = services; 53 | 54 | 55 | return { 56 | DailyPlatform:DailyPlatform 57 | } 58 | } -------------------------------------------------------------------------------- /lib/Sensor.js: -------------------------------------------------------------------------------- 1 | const suncalc = require('suncalc'), 2 | moment = require('moment'), 3 | packageJSON = require("../package.json"), 4 | path = require('path'), 5 | holidays = require('date-holidays'), //https://www.npmjs.com/package/date-holidays#supported-countries-states-regions 6 | ical = require('./iCal.js'), 7 | web = require('./web.js'), 8 | $ = require('./helpers.js'); 9 | 10 | const constantSolarRadiation = 1361 //Solar Constant W/m² 11 | const arbitraryTwilightLux = 6.32 // W/m² egal 800 Lux 12 | let Service, Characteristic; 13 | 14 | 15 | class DailySensor { 16 | constructor(log, config, api, owner) { 17 | this.owner = owner; 18 | 19 | if (!config.location || 20 | !Number.isFinite(config.location.latitude) || 21 | !Number.isFinite(config.location.longitude)) { 22 | throw new Error('Daylight Sensors need a location to work properly'); 23 | } 24 | moment.locale(config.locale ? config.locale : 'en'); 25 | 26 | this.math = new (require('./mymath.js'))(this); 27 | const self = this; 28 | this.config = config === undefined ? {} : config; 29 | this.debug = this.config.debug === undefined ? false : this.config.debug; 30 | 31 | if (log.debug !== undefined) this.log = log; 32 | else this.log = {info:log, debug:this.debug?log:function(){}, error:console.error}; 33 | if (this.config.debug !== undefined) 34 | this.log.debug = this.debug?this.log.info:function(){}; 35 | 36 | this.override = undefined; 37 | this.fixedConfig = true; 38 | this.isActive = false; 39 | this.currentLux = -1; 40 | this.luxService = undefined; 41 | this.dailyRandom = []; 42 | this._setConfig(config); 43 | this.port = this.config.port ? this.config.port : 0; 44 | this.webPath = path.resolve('/', './' + (this.config.webPath ? this.config.webPath : this.config.name.toLowerCase()) + '/'); 45 | this.bluetooth = this.config.bluetoothSwitch == undefined ? {} : this.config.bluetoothSwitch; 46 | this.bluetooth.type = this.bluetooth.type !== undefined ? $.BluetoothSwitchTypes[this.bluetooth.type] : $.BluetoothSwitchTypes.simple; 47 | this.bluetooth.lastEvent = {when:undefined, state:-1} 48 | 49 | 50 | this.log.debug("Loading Events"); 51 | //get the current event state as well as all future events 52 | let allEvents = this.eventsForDate(new Date(), false); 53 | this.events = []; 54 | this.calendar = []; 55 | this.currentEvent = allEvents[0]; 56 | const NOW = new Date(); 57 | allEvents.forEach(event => { 58 | if (event.when - NOW < 0) { 59 | this.currentEvent = event; 60 | } else { 61 | this.events.push(event); 62 | } 63 | }); 64 | 65 | this.activeDay = undefined; 66 | this.switchService = undefined; 67 | this.luxService = undefined; 68 | if (api) { 69 | // Save the API object as plugin needs to register new accessory via this object 70 | this.api = api; 71 | 72 | // Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories. 73 | // Platform Plugin should only register new accessory that doesn't exist in homebridge after this event. 74 | // Or start discover new accessories. 75 | /*this.api.on('didFinishLaunching', function() { 76 | self.log.info("DidFinishLaunching", this.config.name); 77 | }.bind(this));*/ 78 | } 79 | 80 | this.log.info("Updating Initial State for " + this.config.name); 81 | this.updateState(); 82 | 83 | web.startServerForSensor(this); 84 | 85 | this.log.debug("Finished Initialization"); 86 | } 87 | 88 | webCallback(json){ 89 | return false; 90 | } 91 | 92 | restartDiscovery() { 93 | if (this.owner && this.owner.ble){ 94 | this.log.info("Should restart BLE Discovery"); 95 | this.owner.ble.restartDiscovery(); 96 | } 97 | } 98 | 99 | receivedSwitchEvent(data){ 100 | this.log.info("Handling Event", data); 101 | if (data.state !== undefined){ 102 | this.bluetooth.lastEvent = { 103 | when:new Date(), 104 | state:data.state 105 | }; 106 | 107 | if (this.bluetooth.type == $.BluetoothSwitchTypes.triggered) { 108 | this.updateState(this.bluetooth.lastEvent.when) 109 | } else { 110 | this.override = data.state; 111 | this.syncSwitchState(); 112 | } 113 | } 114 | } 115 | 116 | setConfig(config){ 117 | this._setConfig(config); 118 | 119 | this.switchService 120 | .getCharacteristic(Characteristic.ProgrammableSwitchEvent) 121 | .setProps({ minValue:0, maxValue: this.bluetooth.type == $.BluetoothSwitchTypes.simple ? 1 : 2 }); 122 | 123 | this.fetchEvents(new Date()); 124 | this.updateState(); 125 | } 126 | 127 | _setConfig(config){ 128 | this.log.info("Updating Config for " + config.name); 129 | this.config = config; 130 | this.debug = this.config.debug || false; 131 | this.timeout = this.config.tickTimer ? this.config.tickTimer : 30000; 132 | this.dayStart = this.config.dayStartsActive ? this.config.dayStartsActive : false; 133 | if (this.config.location.country === undefined) { 134 | this.holidays = { 135 | isHoliday:function(date) { return false;} 136 | } 137 | } else { 138 | this.holidays = new holidays(this.config.location.country, this.config.location.state, this.config.location.town); 139 | } 140 | 141 | 142 | this.parseTrigger(config.trigger); 143 | } 144 | 145 | getIsActive() { 146 | return (this.override!==undefined) ? (this.override==0 ? false : true) : this.isActive; 147 | } 148 | 149 | configureAccesory(acc){ 150 | let infoService = acc.getService(Service.AccessoryInformation); 151 | let switchService = acc.getService(Service.StatelessProgrammableSwitch); 152 | let lusService = switchService; //acc.getService(Service.LightSensor); 153 | this.configureServices(infoService, lusService, switchService); 154 | } 155 | 156 | configureServiceCharacteristics(informationService, luxService, switchService){ 157 | const when = new Date(); 158 | const pos = this.posForTime(when); 159 | const newLux = this.luxForTime(when, pos); 160 | this.currentLux = Math.round(newLux); 161 | 162 | informationService 163 | .setCharacteristic(Characteristic.Manufacturer, "Ambertation") 164 | .setCharacteristic(Characteristic.Model, "Daily Sensor") 165 | .setCharacteristic(Characteristic.SerialNumber, "0000") 166 | .setCharacteristic(Characteristic.FirmwareRevision, packageJSON.version); 167 | 168 | if (!this.config.noPowerState) 169 | switchService.addCharacteristic(Characteristic.On); 170 | if (!this.config.noLux) 171 | switchService.addCharacteristic(Characteristic.CurrentAmbientLightLevel); 172 | 173 | this.configureServices(informationService, luxService, switchService) 174 | } 175 | 176 | configureServices(informationService, luxService, switchService){ 177 | const self = this; 178 | 179 | if (!self.config.noLux) { 180 | luxService 181 | .getCharacteristic(Characteristic.CurrentAmbientLightLevel) 182 | .on('get', callback => callback(null, self.currentLux)); 183 | 184 | luxService.setCharacteristic( 185 | Characteristic.CurrentAmbientLightLevel, 186 | this.currentLux 187 | ); 188 | 189 | luxService 190 | .getCharacteristic(Characteristic.CurrentAmbientLightLevel) 191 | .setProps({ perms: [Characteristic.Perms.READ] }); 192 | } 193 | 194 | if (!self.config.noPowerState) { 195 | switchService 196 | .getCharacteristic(Characteristic.On) 197 | .on('get', callback => callback(null, self.getIsActive())); 198 | 199 | switchService 200 | .getCharacteristic(Characteristic.On) 201 | .setProps({ perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] }); 202 | } 203 | 204 | switchService 205 | .getCharacteristic(Characteristic.ProgrammableSwitchEvent) 206 | .setProps({ minValue:0, maxValue: this.bluetooth.type == $.BluetoothSwitchTypes.simple ? 1 : 2 }); 207 | 208 | switchService 209 | .getCharacteristic(Characteristic.ServiceLabelIndex) 210 | .setValue(1); 211 | 212 | 213 | this.switchService = switchService; 214 | this.luxService = luxService; 215 | //this.luxService.internalName = this.name + " (Lux)"; 216 | this.informationService = informationService; 217 | 218 | 219 | this.updateState() 220 | this.syncSwitchState(); 221 | } 222 | 223 | getServices(useInfo) { 224 | if (this.switchService == undefined){ 225 | //Info about this plugin 226 | let informationService = useInfo; 227 | if (!useInfo) informationService = new Service.AccessoryInformation (); 228 | 229 | //informationService.subtype = "info"; 230 | //this.luxService = new Service.LightSensor(); 231 | let switchService = new Service.StatelessProgrammableSwitch(this.config.name, this.config.name); 232 | 233 | let luxService = switchService;//new Service.LightSensor(this.config.name, 'lux'); 234 | 235 | this.configureServiceCharacteristics(informationService, luxService, switchService); 236 | } 237 | 238 | return [this.informationService, this.switchService/*, this.luxService*/]; 239 | } 240 | 241 | identify(callback) { 242 | this.log.info('Identify requested!'); 243 | callback(null); 244 | } 245 | 246 | parseTrigger(trigger){ 247 | if (trigger===undefined) trigger = []; 248 | this.triggers = [] 249 | let ID = 0; 250 | trigger.forEach(val => { 251 | const type = $.TriggerTypes[val.type]; 252 | const op = val.op !== undefined ? $.TriggerOps[val.op] : $.TriggerOps.set; 253 | let value = ''; 254 | let random = val.random; 255 | ID++; 256 | let constants; 257 | switch(type){ 258 | case $.TriggerTypes.event: 259 | value = $.EventTypes[val.value]; 260 | break; 261 | case $.TriggerTypes.time: 262 | value = moment(val.value, ['h:m a', 'H:m']).toDate(); 263 | break; 264 | case $.TriggerTypes.altitude: 265 | value = (val.value / 180.0) * Math.PI; 266 | random = (val.random / 180.0) * Math.PI; 267 | //suncalc.addTime(val.value, ID+'_AM', ID+'_PM'); 268 | break; 269 | case $.TriggerTypes.lux: 270 | value = Math.round(val.value); 271 | break; 272 | case $.TriggerTypes.calendar: 273 | value = val.value; //regex 274 | break; 275 | case $.TriggerTypes.holiday: 276 | value = val.value; //array 277 | break; 278 | case $.TriggerTypes.expression: 279 | val.active = val.active == undefined ? true : val.active; 280 | val.trigger = val.trigger == undefined ? 'both' : val.trigger; 281 | 282 | //we need this cause we cannot bind the context to our methods. 283 | //we know that falling back to string replacements is a very bad idea :() 284 | var withSelf = val.value.replace(/(Time\()\s*(['"])\s*([\d:. ]+(am|pm)?)\s*(\2)\s*(\))/gm, '$1self, $2$3$5$6'); 285 | withSelf = withSelf.replace(/(dailyrandom\()\s*(\))/gm, '$1self$2'); 286 | withSelf = withSelf.replace(/(dailyrandom\()\s*([+-]?\d+(\.\d+)?)\s*((,)\s*([+-]?\d+(\.\d+)?))?\s*(\))/gm, '$1self, $2$5$6$8'); 287 | this.log.debug("Rewrote expression:", withSelf); 288 | value = this.math.compile(withSelf); //string 289 | constants = {}; 290 | for(var n in val.constants) { 291 | const r = /(Time\()\s*([\d:. ]+(am|pm)?)\s*(\))/gm 292 | let v = val.constants[n]; 293 | if (val.constants[n] !== undefined && val.constants[n].replace) { 294 | if (v.match(r)){ 295 | v = new this.math.Time(this, val.constants[n].replace(r, '$2')); 296 | } 297 | } 298 | constants[n] = v; 299 | } 300 | break; 301 | default: 302 | return; 303 | } 304 | 305 | let daysOfWeek = 0; 306 | if (val.daysOfWeek !== undefined){ 307 | val.daysOfWeek.forEach(v => { 308 | let d = moment().isoWeekday(v).isoWeekday()-1; 309 | let b = 1 << d; 310 | daysOfWeek |= b; 311 | }); 312 | } 313 | this.triggers.push({ 314 | type: type, 315 | active: val.active !== undefined ? val.active : true, 316 | value: value, 317 | id:ID, 318 | when: $.TriggerWhen[val.trigger ? val.trigger : 'greater'], 319 | op:op, 320 | random: random, 321 | daysOfWeek: daysOfWeek, 322 | constants:constants 323 | }); 324 | }); 325 | this.log.debug(this.triggers); 326 | } 327 | 328 | luxForTime(when, pos){ 329 | if (pos === undefined) { 330 | pos = this.posForTime(when); 331 | } 332 | const minRad = (-9.0 / 180) * Math.PI; 333 | var alt = pos.altitude; 334 | if (alt < minRad) return 0; 335 | 336 | alt -= minRad; 337 | alt /= (Math.PI/2 - minRad); 338 | alt *= Math.PI/2; 339 | 340 | 341 | //this.log.info(pos.altitude- alt, minRad, Math.sin(alt) * 10000); 342 | return Math.max(0.0001, Math.sin(alt) * 10000); 343 | } 344 | 345 | //https://web.archive.org/web/20170819110438/http://www.domoticz.com:80/wiki/Real-time_solar_data_without_any_hardware_sensor_:_azimuth,_Altitude,_Lux_sensor... 346 | luxForTime2(when, pos){ 347 | const numOfDay = moment(when).dayOfYear(); 348 | const nbDaysInYear = 365; 349 | const RadiationAtm = constantSolarRadiation * (1 +0.034 * Math.cos((Math.PI / 180) * numOfDay / nbDaysInYear )); // Sun radiation (in W/m²) in the entrance of atmosphere. 350 | if (pos === undefined) { 351 | pos = this.posForTime(when); 352 | } 353 | const sinusSunAltitude = Math.sin(pos.altitude); 354 | const altitude = 300; 355 | const relativePressure = 1100; 356 | const absolutePressure = relativePressure - Math.round((altitude/ 8.3),1) // hPa 357 | const M0 = Math.sqrt(1229 + Math.pow(614 * sinusSunAltitude,2)) - 614 * sinusSunAltitude 358 | const M = M0 * relativePressure/absolutePressure 359 | if (pos.altitude > Math.PI/180) { 360 | const directRadiation = RadiationAtm * Math.pow(0.6,M) * sinusSunAltitude; 361 | const scatteredRadiation = RadiationAtm * (0.271 - 0.294 * Math.pow(0.6,M)) * sinusSunAltitude; 362 | const totalRadiation = scatteredRadiation + directRadiation; 363 | const Lux = totalRadiation / 0.0079 //Radiation in Lux. 1 Lux = 0,0079 W/m² 364 | return Lux; 365 | } else if (pos.altitude <= Math.PI/180 && pos.altitude >= -7*Math.PI/180) { 366 | const directRadiation = 0 367 | const scatteredRadiation = 0 368 | const arbitraryTwilight=arbitraryTwilightLux-(1-pos.altitude)/8*arbitraryTwilightLux 369 | const totalRadiation = scatteredRadiation + directRadiation + arbitraryTwilight 370 | const Lux = totalRadiation / 0.0079 // Radiation in Lux. 1 Lux = 0,0079 W/m² 371 | return Lux; 372 | } else { 373 | return 0; 374 | } 375 | } 376 | 377 | eventsForDate(when, commingOnly){ 378 | if (commingOnly === undefined) commingOnly = true; 379 | const times = suncalc.getTimes(when, this.config.location.latitude, this.config.location.longitude); 380 | const NOW = new Date(); 381 | let events = []; 382 | 383 | for (var property in times) { 384 | if (times.hasOwnProperty(property)) { 385 | const time = times[property]; 386 | const delta = time-NOW; 387 | if (delta>=0 || !commingOnly) { 388 | const pos = this.posForTime(time); 389 | 390 | events.push({ 391 | event: property, 392 | when: time, 393 | lux: this.luxForTime(time, pos), 394 | pos: pos 395 | }); 396 | } 397 | } 398 | } 399 | events.sort(function(a, b) { return a.when - b.when; }); 400 | return events; 401 | } 402 | 403 | posForTime(when){ 404 | return suncalc.getPosition(when, this.config.location.latitude, this.config.location.longitude); 405 | } 406 | 407 | fetchEventAt(when){ 408 | var result = undefined; 409 | this.events.forEach(event => { 410 | if (event.when - when < 0) { 411 | result = event; 412 | } 413 | }); 414 | 415 | return result; 416 | } 417 | 418 | fetchEvents(when) { 419 | this.calendar = []; 420 | if (this.config.calendar) { 421 | ical.loadEventsForDay(moment(when), this.config.calendar, (list, start, end) => { 422 | this.log.debug("New Calendar Events:\n", list.map(e => " " + moment(e.startDate).format('LTS') + " - " + moment(e.endDate).format('LTS') + ": "+ e.summary).join("\n")); 423 | 424 | this.calendar = list; 425 | }); 426 | } 427 | 428 | var e1 = this.eventsForDate(when, false); 429 | var e2 = this.eventsForDate(moment().add(1, 'day').toDate(), false); 430 | var e0 = this.eventsForDate(moment().add(-1, 'day').toDate(), false); 431 | 432 | this.events = e0.concat(e1).concat(e2); 433 | this.log.debug(moment(when).format('LTS')); 434 | this.events.forEach(event => { 435 | this.log.debug(moment(event.when).format('LTS'), event.event, $.formatRadians(event.pos.altitude), Math.round(event.lux)); 436 | }); 437 | 438 | this.dailyRandom = []; 439 | 440 | this.triggers.forEach(trigger => { 441 | let r = trigger.random ? trigger.random : 0; 442 | if (r==0){ 443 | trigger.randomizedValue = trigger.value; 444 | return; 445 | } 446 | 447 | let rnd = Math.random() * 2*r - r; 448 | switch (trigger.type ) { 449 | case $.TriggerTypes.lux: 450 | case $.TriggerTypes.altitude: 451 | trigger.randomizedValue = trigger.value + rnd; 452 | break; 453 | case $.TriggerTypes.time: 454 | let m = moment(trigger.value); 455 | m = m.add(rnd, 'minutes'); 456 | trigger.randomizedValue = m.toDate(); 457 | break; 458 | default: 459 | trigger.randomizedValue = trigger.value 460 | } 461 | 462 | this.log.debug("generated", trigger.randomizedValue, "from", trigger.value, "+", trigger.random, rnd) 463 | }); 464 | } 465 | 466 | matchesCalEventNow(when, regex) { 467 | const r = new RegExp(regex); 468 | 469 | const events = ical.eventsAt(moment(when), this.calendar).filter(e => e.summary.match(r)!==null); 470 | this.log.debug("Matching Events for '" + regex + "' at "+ moment(when).format('LTS') +":\n", events.map(e => " " + moment(e.startDate).format('LTS') + " - " + moment(e.endDate).format('LTS') + ": "+ e.summary).join("\n")); 471 | 472 | return events.length > 0; 473 | } 474 | 475 | isHoliday(when, types) { 476 | if (types === undefined) types = ['public', 'bank']; 477 | if (types.length === undefined) types = [types]; 478 | const h = this.holidays.isHoliday(when); 479 | //this.log.info(time.toString(), h); 480 | if (h !== false){ 481 | return types.indexOf(h.type)>=0; 482 | } else { 483 | return false; 484 | } 485 | } 486 | 487 | queueNextEvent() { 488 | const now = moment(); 489 | const day = moment({h: 0, m: 0, s: 1}); 490 | var days = this.activeDay ? Math.abs(moment.duration(day.diff(this.activeDay)).asDays()) : 1; 491 | this.log.debug("Curent Event: ", this.fetchEventAt(now.toDate()), "days passed", days); 492 | if (days >= 0.98) { 493 | const when = now.toDate(); 494 | this.activeDay = day; 495 | this.fetchEvents(when); 496 | } 497 | 498 | setTimeout(this.updateState.bind(this, undefined), this.timeout); 499 | } 500 | 501 | testTrigger(trigger, when, obj, result, single, silent) { 502 | const self = this; 503 | 504 | if (trigger.daysOfWeek != 0) { 505 | const dow = 1 << (moment(when).isoWeekday() - 1); 506 | if ((trigger.daysOfWeek & dow) == 0) { 507 | return result; 508 | } 509 | } 510 | 511 | function concat(r) { 512 | if (single) { 513 | result = r; 514 | return; 515 | } 516 | switch(trigger.op){ 517 | case $.TriggerOps.and: 518 | result = result && r; 519 | break; 520 | case $.TriggerOps.or: 521 | result = result || r; 522 | break; 523 | case $.TriggerOps.discard: 524 | break; 525 | default: 526 | result = r; 527 | } 528 | } 529 | 530 | function changeByTrigger(trigger, what){ 531 | if (what && (trigger.when == $.TriggerWhen.greater || trigger.when == $.TriggerWhen.both)) { 532 | 533 | concat(trigger.active); 534 | if (!silent) obj.conditions.push({trigger:trigger, active:trigger.active, result:result}); 535 | if (!silent) self.log.debug(" Trigger changed result -- " + $.formatTrigger(trigger) + " => " + result); 536 | } else if (!what && (trigger.when == $.TriggerWhen.less || trigger.when == $.TriggerWhen.both)) { 537 | concat(!trigger.active); 538 | if (!silent) obj.conditions.push({trigger:trigger, active:!trigger.active, result:result}); 539 | if (!silent) self.log.debug(" Trigger changed result -- " + $.formatTrigger(trigger) + " => " + result); 540 | } 541 | } 542 | 543 | switch(trigger.type) { 544 | case $.TriggerTypes.time: 545 | changeByTrigger(trigger, $.justTime(when) > $.justTime(trigger.randomizedValue)); 546 | break; 547 | case $.TriggerTypes.event: 548 | const event = this.fetchEventAt(when); 549 | if (event) { 550 | changeByTrigger(trigger, $.EventTypes[event.event] == trigger.value); 551 | } 552 | break; 553 | case $.TriggerTypes.altitude: 554 | changeByTrigger(trigger, obj.pos.altitude > trigger.randomizedValue ); 555 | break; 556 | case $.TriggerTypes.lux: 557 | changeByTrigger(trigger, obj.lux > trigger.randomizedValue ); 558 | break; 559 | case $.TriggerTypes.calendar: 560 | changeByTrigger(trigger, this.matchesCalEventNow(when, trigger.value) ); 561 | break; 562 | case $.TriggerTypes.holiday: 563 | changeByTrigger(trigger, this.isHoliday(when, trigger.value) ); 564 | break; 565 | case $.TriggerTypes.expression: 566 | const res = trigger.value.run(trigger.constants, when); 567 | if (typeof(res)==='boolean') { 568 | changeByTrigger(trigger, res); 569 | } else { 570 | this.log.debug("Math Expression forced override value '"+res+"'"); 571 | this.override = res; 572 | } 573 | break; 574 | default: 575 | 576 | } 577 | 578 | return result; 579 | } 580 | 581 | testIfActive(when, triggerList) { 582 | if (triggerList === undefined) triggerList = this.triggers; 583 | const pos = this.posForTime(when); 584 | const newLux = this.luxForTime(when, pos); 585 | let obj = { 586 | active:false, 587 | pos:pos, 588 | lux:newLux, 589 | conditions:[] 590 | }; 591 | 592 | const self = this; 593 | let result = this.dayStart; 594 | this.log.debug("Starting day with result -- " + result); 595 | triggerList.forEach(trigger => result = self.testTrigger(trigger, when, obj, result, false, false)); 596 | 597 | obj.active = result; 598 | return obj; 599 | } 600 | 601 | updateState(when) { 602 | if (this.switchService == undefined) return; 603 | if (when === undefined) when = new Date(); 604 | 605 | //make sure the switch has the same state 606 | this.currentSwitchValue = this.switchService 607 | .getCharacteristic(Characteristic.ProgrammableSwitchEvent) 608 | .value; 609 | 610 | if (this.override === undefined) { 611 | if (this.currentSwitchValue != Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS && !this.getIsActive()) { 612 | this.log.debug("FORCE SEND DOUBLE_PRESS"); 613 | this.syncSwitchState(); 614 | } else if (this.currentSwitchValue != Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS && this.currentSwitchValue != Characteristic.ProgrammableSwitchEvent.LONG_PRESS && this.getIsActive()) { 615 | this.log.debug("FORCE SEND SINGLE_PRESS"); 616 | this.syncSwitchState(); 617 | } else { 618 | //this.syncSwitchState(); 619 | } 620 | //this.log.info("STATE", val, Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS); 621 | } 622 | 623 | const obj = this.testIfActive(when); 624 | const pos = obj.pos; 625 | const newLux = obj.lux; 626 | const result = obj.active; 627 | 628 | const self = this; 629 | 630 | if (!this.config.noLux && this.luxService && Math.abs(this.currentLux - newLux)>1){ 631 | this.currentLux = Math.round(newLux); 632 | this.luxService.setCharacteristic( 633 | Characteristic.CurrentAmbientLightLevel, 634 | this.currentLux 635 | ); 636 | } 637 | 638 | if (this.isActive != result) { 639 | this.override = undefined; 640 | this.isActive = result; 641 | this.syncSwitchState(); 642 | } 643 | 644 | this.log.debug(" State at " + moment(when).format('LTS'), this.isActive, this.currentLux); 645 | this.queueNextEvent(); 646 | } 647 | 648 | syncSwitchState(){ 649 | if (!this.config.noPowerState){ 650 | this.switchService.setCharacteristic( 651 | Characteristic.On, 652 | this.getIsActive() 653 | ); 654 | } 655 | 656 | let action = 0; 657 | if (this.bluetooth.type == $.BluetoothSwitchTypes.simple || this.override === undefined || this.override < 2) { 658 | action = this.getIsActive() ? Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS : Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS; 659 | } if (this.bluetooth.type != $.BluetoothSwitchTypes.simple && this.override == 2) { 660 | action = Characteristic.ProgrammableSwitchEvent.LONG_PRESS; 661 | } 662 | this.log.debug("Sending", action==Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS?'SINGLE':(action==Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS?'DOUBLE':(action==Characteristic.ProgrammableSwitchEvent.LONG_PRESS?'LONG':action))) 663 | 664 | this.switchService.updateCharacteristic(Characteristic.ProgrammableSwitchEvent, action); 665 | /*this.switchService 666 | .getCharacteristic(Characteristic.ProgrammableSwitchEvent) 667 | .setValue(action);*/ 668 | 669 | this.currentSwitchValue = this.switchService 670 | .getCharacteristic(Characteristic.ProgrammableSwitchEvent) 671 | .value; 672 | } 673 | 674 | iterateDay(cb, startYesterday, deltaMinutes){ 675 | if (startYesterday === undefined) startYesterday = false; 676 | if (deltaMinutes === undefined) deltaMinutes = 1; 677 | let iterations = Math.round(24*60 / deltaMinutes) + (startYesterday?2:1); 678 | let time = moment().startOf('day').subtract(startYesterday?deltaMinutes:0, 'minutes'); 679 | 680 | while (iterations>0) { 681 | cb(iterations, time); 682 | 683 | time.add(deltaMinutes, 'minutes'); 684 | iterations--; 685 | } 686 | } 687 | } 688 | 689 | module.exports = function(service, characteristic){ 690 | Service = service; 691 | Characteristic = characteristic; 692 | 693 | return { 694 | DailySensor:DailySensor 695 | } 696 | } -------------------------------------------------------------------------------- /lib/Service.js: -------------------------------------------------------------------------------- 1 | const packageJSON = require("../package.json"), 2 | path = require('path'), 3 | web = require('./web.js'), 4 | $ = require('./helpers.js'); 5 | 6 | let Service, Characteristic, Accessory, UUIDGen, DailySensor, DailyGroup, DailySocket, DailyLight; 7 | 8 | let knownAccesories = []; 9 | let knownPlatforms = []; 10 | class DailyService { 11 | constructor(log, config, api) { 12 | const self = this; 13 | 14 | this.config = config || {}; 15 | this.debug = this.config.debug || false; 16 | this.name = this.config && this.config.name ? this.config.name : "Daily Platform"; 17 | this.UUID = UUIDGen.generate("DailyPlatform." + this.name); 18 | knownPlatforms.push(this); 19 | //console.log("LST.INIT", this.name, this.UUID, process.argv); 20 | this.log = {info:log, debug:this.debug?log:function(){}, error:console.error}; 21 | 22 | this.accessories = []; 23 | this.removeAfterStartup = []; 24 | this.port = this.config.port ? this.config.port : 0; 25 | this.webPath = path.resolve('/', './' + (this.config.webPath ? this.config.webPath : this.name.toLowerCase()) + '/'); 26 | this.btDevices = this.config.bluetoothDevices !== undefined ? this.config.bluetoothDevices : []; 27 | this.ble = {}; 28 | if (this.config && this.config.bluetoothDevices && this.config.bluetoothDevices.length > 0){ 29 | try { 30 | this.ble = require('./ble.js')( 31 | this.log, 32 | this.btDevices, 33 | this.btEvent.bind(this) 34 | ); 35 | } catch(e){ 36 | this.log.error("Could Not Start Noble", e); 37 | } 38 | } 39 | 40 | if (api) { 41 | // Save the API object as plugin needs to register new accessory via this object 42 | this.api = api; 43 | 44 | // Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories. 45 | // Platform Plugin should only register new accessory that doesn't exist in homebridge after this event.x 46 | // Or start discover new accessories. 47 | this.api.on('didFinishLaunching', function() { 48 | self.log.info("DidFinishLaunching " + this.config.name, this.removeAfterStartup.length); 49 | 50 | try { 51 | this.api.unregisterPlatformAccessories("homebridge-daily-sensors", "DailyPlatform", this.removeAfterStartup); 52 | } catch(e){ 53 | this.log.error(e) 54 | } 55 | this.removeAfterStartup = undefined; 56 | 57 | if (this.config.accessories) { 58 | this.config.accessories.forEach(accConf => { 59 | let acc = this.getKnownAccessoryWithName(accConf.name); 60 | 61 | if (acc){ 62 | this.log.debug("Reconfigure existing Accessory", accConf.name); 63 | 64 | acc = this.configureAccessoryOnPlatform(acc); 65 | acc.setConfig(this.buildConfig(accConf)); 66 | acc.platformAccessory.context.userConfig = accConf; 67 | } else { 68 | this.log.debug("Creating new Accessory", accConf.name); 69 | acc = this.addAccessory(accConf); 70 | } 71 | if (acc) acc.fixedConfig = true; 72 | }) 73 | } 74 | 75 | if (this.ble.initAll){ 76 | this.log.info("Initializing BLE"); 77 | this.ble.initAll(); 78 | } 79 | }.bind(this)); 80 | } 81 | } 82 | 83 | btEvent(e){ 84 | //this.accessories.forEach(sensor => console.log(sensor.btSwitch, sensor.config.name)) 85 | this.log.debug("Event:", e); 86 | if (e!==undefined && e.id !== undefined){ 87 | this.accessories 88 | .filter(sensor => ( 89 | sensor instanceof DailyGroup || 90 | (sensor.bluetooth && sensor.bluetooth.id === e.id) 91 | )) 92 | .forEach(sensor => sensor.receivedSwitchEvent(e)); 93 | } 94 | } 95 | 96 | buildConfig(accConfig){ 97 | let conf = { 98 | locale:this.config.locale, 99 | port:this.port, 100 | debug:this.debug, 101 | location:this.config.location, 102 | ...accConfig 103 | } 104 | 105 | conf.webPath = path.resolve('/' + this.webPath + '/' + conf.webPath); 106 | 107 | return conf; 108 | } 109 | 110 | linkSensor(sensor){ 111 | //bluetoothSwitchID 112 | this.accessories.push(sensor); 113 | } 114 | 115 | createAccesory(config){ 116 | if (config.accessory == 'DailySensors') { 117 | return new DailySensor(this.log, config, this.api, this); 118 | } else if (config.accessory == 'DailyGroup') { 119 | return new DailyGroup(this.log, config, this.api, this); 120 | } else if (config.accessory == 'DailySocket') { 121 | return new DailySocket(this.log, config, this.api, this); 122 | } else if (config.accessory == 'DailyLight') { 123 | return new DailyLight(this.log, config, this.api, this); 124 | } 125 | this.log.error(" ... Unknown Accessory type '" + config.accessory + "'"); 126 | return undefined; 127 | } 128 | 129 | configureAccessory(accessory) { 130 | //console.log("LST.READ", accessory.displayName, accessory.UUID, accessory.context.platformUUID, '@', this.name, this.UUID); 131 | 132 | const pp = knownPlatforms.find(p=>p.UUID === accessory.context.platformUUID ); 133 | 134 | if (process.argv.some(v=>v==='-Q') && pp && !pp.config.accessories.some(v=>v.name == accessory.context.userConfig.name)) { 135 | //console.log("LST.D1", pp.name, accessory.displayName, accessory.UUID, accessory.context.wasUsedInPlatform, accessory.context.platformUUID); 136 | this.log.error("Did no longer find '"+accessory.displayName+"'. Removing from list."); 137 | this.removeAfterStartup.push(accessory); 138 | return false; 139 | } else if ((knownPlatforms.length>0 || process.argv.some(v=>v==='-Q')) && !knownPlatforms.some(v => v.UUID === accessory.context.platformUUID)) { 140 | //console.log("LST.D2", pp.name, accessory.displayName, accessory.UUID, accessory.context.wasUsedInPlatform, accessory.context.platformUUID); 141 | this.log.error("WILL REMOVE", accessory.displayName); 142 | this.removeAfterStartup.push(accessory); 143 | return false; 144 | } else if (accessory.context.userConfig.accessory == 'DailyGroup'){ 145 | //console.log("LST.D3", pp.name, accessory.displayName, accessory.UUID, accessory.context.wasUsedInPlatform, accessory.context.platformUUID); 146 | this.removeAfterStartup.push(accessory); 147 | return false; 148 | } 149 | 150 | //console.log("LST.PUSH", accessory.displayName, accessory.UUID, accessory.context.platformUUID, '@', this.name, this.UUID); 151 | accessory.context.wasUsedInPlatform = false; 152 | 153 | knownAccesories.push(accessory); 154 | return true; 155 | } 156 | 157 | configureAccessoryOnPlatform(accessory){ 158 | //console.log("LST.CONF.P", accessory.displayName, accessory.UUID, accessory.context.platformUUID, '@', this.name, this.UUID); 159 | 160 | accessory.context.wasUsedInPlatform = true; 161 | this.log.info("Configure Accessory", accessory.displayName); 162 | var platform = this; 163 | 164 | const config = this.buildConfig(accessory.context.userConfig); 165 | //this.log.debug(" Configuration", config); 166 | const sensor = this.createAccesory(config); 167 | if (sensor === undefined) { 168 | this.log.error(" ... Falling back to DailySensors"); 169 | sensor = new DailySensor(this.log, config, this.api, this); 170 | } 171 | 172 | sensor.platformAccessory = accessory; 173 | sensor.fixedConfig = false; 174 | 175 | accessory.on('identify', function(paired, callback) { 176 | sensor.identify(); 177 | callback(); 178 | }); 179 | 180 | sensor.configureAccesory(accessory); 181 | this.linkSensor(sensor); 182 | 183 | return sensor; 184 | } 185 | 186 | addAccessory(inConfig) { 187 | const accessoryName = inConfig.name; 188 | this.log.info("Add Accessory", accessoryName); 189 | 190 | const uuid = UUIDGen.generate(accessoryName); 191 | //console.log("LST.ADD", accessoryName, uuid, '@', this.name, this.UUID); 192 | const newAccessory = new Accessory(accessoryName, uuid); 193 | const infoService = newAccessory.getService(Service.AccessoryInformation); 194 | 195 | const config = this.buildConfig(inConfig); 196 | const sensor = this.createAccesory(config); 197 | if (sensor === undefined) return; 198 | 199 | 200 | sensor.platformAccessory = newAccessory; 201 | sensor.fixedConfig = false; 202 | 203 | newAccessory.on('identify', function(paired, callback) { 204 | sensor.identify(); 205 | callback(); 206 | }); 207 | // Plugin can save context on accessory to help restore accessory in configureAccessory() 208 | newAccessory.context.userConfig = inConfig; 209 | newAccessory.context.platformUUID = this.UUID; 210 | newAccessory.context.wasUsedInPlatform = true; 211 | 212 | const services = sensor.getServices(infoService); 213 | let nr = 0; 214 | 215 | services.forEach(service=>{ 216 | if (infoService.UUID != service.UUID) { 217 | newAccessory.addService( 218 | service, 219 | accessoryName + " (" + service.internalName + ")" 220 | ); 221 | } 222 | }); 223 | 224 | this.linkSensor(sensor); 225 | this.api.registerPlatformAccessories("homebridge-daily-sensors", "DailyPlatform", [newAccessory]); 226 | 227 | return sensor; 228 | } 229 | 230 | removeAccessory(accessory) { 231 | this.log.info("Remove Accessory", accessory.displayName, accessory.UUID); 232 | this.api.unregisterPlatformAccessories("homebridge-daily-sensors", "DailyPlatform", [accessory]); 233 | 234 | this.accessories = this.accessories.filter(sensor => sensor.platformAccessory.UUID != accessory.UUID); 235 | this.log.debug(this.accessories); 236 | } 237 | 238 | getKnownAccessoryWithName(name){ 239 | return knownAccesories.find(acc => acc.displayName == name); 240 | } 241 | 242 | getAccessoryWithName(name) { 243 | return this.accessories.find(acc => acc.config.name == name); 244 | } 245 | 246 | hasAccessoryWithName(name){ 247 | return this.accessories.some(acc => acc.config.displayName == name); 248 | } 249 | } 250 | 251 | module.exports = function(service, characteristic, accessory, uuidGen, dailySensor, dailyGroup, dailySocket, dailyLight){ 252 | Service = service; 253 | Characteristic = characteristic; 254 | Accessory = accessory; 255 | UUIDGen = uuidGen; 256 | DailySensor = dailySensor; 257 | DailyGroup = dailyGroup; 258 | DailySocket = dailySocket; 259 | DailyLight = dailyLight; 260 | 261 | 262 | return { 263 | DailyService:DailyService, 264 | knownAccesories:knownAccesories, 265 | knownPlatforms:knownPlatforms 266 | } 267 | } -------------------------------------------------------------------------------- /lib/ble.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // based on the noble code from homebridge-avea-bulb 3 | 4 | const moment = require('moment'), 5 | packageJSON = require("../package.json"), 6 | path = require('path'), 7 | Noble = require('noble'), 8 | fs = require('fs'), 9 | $ = require('./helpers.js'), 10 | bleDailySwitch = require('./bleDailySwitch.js'); 11 | 12 | var scanRestartCount = 0; 13 | const maxScanRestarts = 5; 14 | var watchdogCallCount = 0; 15 | const watchdogCallsBeforeStop = 3; 16 | const DISCOVERY_RUN_TIME = 20000; 17 | const DISCOVERY_PAUSE_TIME = 5000; 18 | const WATCHDOG_TIMER = 23000; 19 | const WATCHDOG_RESTART_TIMER = 1000; 20 | const serviceUUID = '0818c2dd127641059e650d10c8e27e35'; 21 | const switchCharacteristicUUID = "3e989a753437471ea50a20a838afcce7"; 22 | 23 | class BluetoothClient { 24 | constructor(log, devices, callback) { 25 | this.log = log; 26 | this.callback = callback; 27 | this.scanning = false; 28 | this.ready = false; 29 | this.connectionInFlight = 0; 30 | this.queuedStart = null; 31 | this.connectionProgress = false; 32 | this.devices = devices.map(d => {return {id:d, peripheral:undefined, connected:false, discovered:false};}); 33 | 34 | //Initialise the Noble service for talking to enabled devices 35 | //Noble.on('stateChange', this.nobleStateChange.bind(this)); 36 | this.scanStopListener = this.nobleScanStop.bind(this) 37 | //Noble.on('scanStop', this.scanStopListener); 38 | this.watchdog(); 39 | } 40 | 41 | allDiscovered(){ 42 | return !this.devices.some(item => !item.discovered ); 43 | } 44 | 45 | deviceWithID(bid){ 46 | return this.devices.find(item => item.id == bid); 47 | } 48 | 49 | watchdog(){ 50 | this.log.info("Called Watchdog..."); 51 | //make sure we give the device a chance to connect the characteristics properly 52 | if (this.connectionInFlight>0){ 53 | this.connectionInFlight--; 54 | return; 55 | } 56 | if (this.ready && (this.attachedDiscoverCall === undefined || Noble.listenerCount('discover')==0)){ 57 | this.log.info("Watchdog restarted Discovery"); 58 | this.attachedDiscoverCall = this.nobleDiscovered.bind(this); 59 | Noble.on("discover", this.attachedDiscoverCall); 60 | this.startScanningWithTimeout(); 61 | watchdogCallCount = 0; 62 | } else if (this.ready && (this.attachedDiscoverCall !== undefined || Noble.listenerCount('discover')!=0)){ 63 | this.log.info("Watchdog++"); 64 | watchdogCallCount++; 65 | if (watchdogCallCount >= watchdogCallsBeforeStop) { 66 | watchdogCallCount = 0; 67 | this.log.info("Watchdog stops Discovery for", WATCHDOG_RESTART_TIMER); 68 | this.stopScanning(); 69 | 70 | setTimeout(this.watchdog.bind(this), WATCHDOG_RESTART_TIMER) 71 | return; 72 | } 73 | } else { 74 | this.log.info("watchdog ignored:", this.ready, this.attachedDiscoverCall===undefined, Noble.listenerCount('discover')); 75 | } 76 | setTimeout(this.watchdog.bind(this), WATCHDOG_TIMER) 77 | } 78 | 79 | startConnection(device){ 80 | this.log.info("Will connect", device.id); 81 | try { 82 | if (device.connected) { 83 | this.log.info("Already Connected", device.id); 84 | return; 85 | } 86 | bleDailySwitch.connect(this, device, serviceUUID, switchCharacteristicUUID); 87 | } catch (error){ 88 | this.log.error("Unhandled Error occured", error); 89 | } 90 | } 91 | 92 | // Noble State Change 93 | nobleStateChange (state) { 94 | if (state == "poweredOn") { 95 | this.ready = true; 96 | this.log.info("Starting Noble scan.."); 97 | 98 | if (this.attachedDiscoverCall === undefined) { 99 | this.attachedDiscoverCall = this.nobleDiscovered.bind(this); 100 | Noble.on("discover", this.attachedDiscoverCall); 101 | } 102 | 103 | this.startScanningWithTimeout(); 104 | this.scanning = true; 105 | } else { 106 | this.ready = false; 107 | this.log.info("Noble state change to " + state + "; stopping scan."); 108 | if (this.scanStopListener !== undefined) { 109 | Noble.removeListener('scanStop', this.scanStopListener); 110 | this.scanStopListener = undefined; 111 | } 112 | if (this.attachedDiscoverCall !== undefined){ 113 | Noble.removeListener('discover', this.attachedDiscoverCall); 114 | this.attachedDiscoverCall = undefined; 115 | } 116 | this.log.debug('State Changed to '+state+'. Stopping Scan for Service ' + serviceUUID); 117 | Noble.stopScanning(); 118 | 119 | this.scanning = false; 120 | } 121 | } 122 | 123 | // Noble Stop Scan 124 | nobleScanStop() { 125 | //this.log.debug('ScanStop received: allFound=' + this.allDiscovered() + ', ct=' + scanRestartCount); 126 | } 127 | 128 | restartDiscovery(){ 129 | if (this.restartTimerCall === undefined) { 130 | this.restartTimerCall = setTimeout(function () { 131 | if (!this.allDiscovered()){ 132 | scanRestartCount++; 133 | this.restartTimerCall = undefined; 134 | this.log.debug('Restarting Discovery allFound=' + this.allDiscovered() + ', ct=' + scanRestartCount + ', where=event'); 135 | this.startScanningWithTimeout(); 136 | } 137 | }.bind(this), DISCOVERY_PAUSE_TIME); 138 | } 139 | } 140 | 141 | ensureScanStopNotification(){ 142 | if (this.scanStopListener !== undefined) { 143 | //someone deleted our listener :() 144 | if (!Noble.listeners('scanStop').some(t=>t===this.scanStopListener)){ 145 | this.log.error("Outsider deleted listener ...restoring"); 146 | this.scanStopListener = this.nobleScanStop.bind(this) 147 | Noble.on('scanStop', this.scanStopListener); 148 | } 149 | } else { 150 | this.scanStopListener = this.nobleScanStop.bind(this) 151 | Noble.on('scanStop', this.scanStopListener); 152 | } 153 | } 154 | 155 | startScanningWithTimeout() { 156 | this.ensureScanStopNotification(); 157 | const self = this; 158 | this.log.info("Start Scanning for Service", serviceUUID); 159 | 160 | Noble.startScanning([serviceUUID], false, (error) => { 161 | if (error) { 162 | this.log.error("Failed to Start Scan:", error); 163 | } else { 164 | this.log.debug("Started Scan"); 165 | } 166 | }); 167 | setTimeout(function () { 168 | if (Noble.listenerCount('discover') == 0) { return; } 169 | this.log.debug('Discovery timeout. Stopping Scan for Service ' + serviceUUID); 170 | this.ensureScanStopNotification(); 171 | Noble.stopScanning(); 172 | this.scanning = false; 173 | 174 | this.restartDiscovery(); 175 | }.bind(this), DISCOVERY_RUN_TIME); 176 | } 177 | 178 | stopScanning() { 179 | if (this.attachedDiscoverCall !== undefined){ 180 | Noble.removeListener('discover', this.attachedDiscoverCall); 181 | this.attachedDiscoverCall = undefined; 182 | } 183 | this.log.debug("Stop Scanning?", Noble.listenerCount('discover') ); 184 | 185 | if (Noble.listenerCount('discover') == 0) { 186 | if (this.restartTimerCall !== undefined){ 187 | clearTimeout(this.restartTimerCall); 188 | this.restartTimerCall = undefined; 189 | } 190 | if (this.scanStopListener !== undefined) { 191 | Noble.removeListener('scanStop', this.scanStopListener); 192 | this.scanStopListener = undefined; 193 | } 194 | this.log.debug('Stopping Scan for Service ' + serviceUUID); 195 | Noble.stopScanning(); 196 | } 197 | } 198 | 199 | // Noble Discovery 200 | nobleDiscovered(peripheral) { 201 | this.log.debug("Discovered:", peripheral.uuid, peripheral.advertisement.localName); 202 | const device = this.deviceWithID(peripheral.uuid); 203 | 204 | if (this.devices.length == 0) { 205 | this.stopScanning(); //its is over (forever) 206 | this.scanning = false; 207 | } else if (device !== undefined && device!==null && device.peripheral===undefined) { 208 | this.connectionInFlight = 5; 209 | device.peripheral = peripheral; 210 | device.discovered = true; 211 | this.log.info("Found new Device ", peripheral.advertisement.localName); 212 | 213 | const self = this; 214 | if (this.allDiscovered()){ 215 | self.stopScanning(); 216 | self.scanning = false; 217 | } 218 | 219 | this.log.info("Will connect", device.id, "in", 3000); 220 | if (this.queuedStart) { 221 | this.log.info("Clean old Timeout"); 222 | clearTimeout(this.queuedStart); 223 | this.queuedStart = null; 224 | } 225 | this.queuedStart = setTimeout(this.startConnection.bind(this, device), 3000); 226 | //this.startConnection(device); 227 | } else if (device !== undefined && device!==null) { 228 | this.connectionInFlight = 5; 229 | this.log.info("Lost device " + device.id + " reappeared!"); 230 | 231 | /*device.peripheral.removeAllListeners('connect'); 232 | device.peripheral.removeAllListeners('disconnect'); 233 | device.peripheral.removeAllListeners('servicesDiscover'); 234 | device.peripheral = undefined;*/ 235 | 236 | if (device.switchCharacteristic){ 237 | device.switchCharacteristic.unsubscribe((err)=>{}); 238 | device.switchCharacteristic.removeAllListeners('data'); 239 | device.switchCharacteristic = undefined; 240 | } 241 | 242 | device.peripheral = peripheral; 243 | device.discovered = true; 244 | if (device.peripheral.state != "connected") { 245 | const self = this; 246 | if (this.allDiscovered()){ 247 | self.stopScanning(); 248 | self.scanning = false; 249 | } 250 | this.log.info("Will connect", device.id, "in", 2000); 251 | if (this.queuedStart) { 252 | this.log.info("Clean old Timeout"); 253 | clearTimeout(this.queuedStart); 254 | this.queuedStart = null; 255 | } 256 | this.queuedStart = setTimeout(this.startConnection.bind(this, device), 2000); 257 | //this.startConnection(device); 258 | } else { 259 | this.log.error(" ... Undefined state"); 260 | } 261 | } else { 262 | this.log.debug(" ... Ignoring", peripheral.uuid, this.devices, device); 263 | } 264 | } 265 | } 266 | 267 | module.exports = function(log, devices, callback){ 268 | return { 269 | object:null, 270 | initAll: function(){} 271 | } 272 | 273 | if (devices.length == 0) return undefined; 274 | log = {info:log.info, debug:log.debug, error:log.info}; 275 | let scanners = { 276 | object:null, 277 | initAll: function(){ 278 | log.info("Init BLE for DailySwitch"); 279 | Noble.on('stateChange', this.object.nobleStateChange.bind(this.object)); 280 | Noble.on('scanStop', this.object.nobleScanStop.bind(this.object)); 281 | 282 | } 283 | }; 284 | devices.forEach(device => { 285 | log.info("Added Scanner for Device", device); 286 | scanners.object = new BluetoothClient(log, [device], callback); 287 | }) 288 | 289 | return scanners; 290 | } -------------------------------------------------------------------------------- /lib/bleDailySwitch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // based on the noble code from homebridge-avea-bulb 3 | 4 | const moment = require('moment'), 5 | packageJSON = require("../package.json"), 6 | path = require('path'), 7 | Noble = require('noble'), 8 | fs = require('fs'), 9 | $ = require('./helpers.js'); 10 | 11 | 12 | const REDISCOVER_TIMEOUT = 500; 13 | const REDISCOVER_MAX_COUNT = 10; 14 | 15 | module.exports.connect = function(ble, device, serviceUUID, switchCharacteristicUUID){ 16 | if (device.connecting) return; 17 | device.connecting = true; 18 | const self = ble; 19 | 20 | let disconnectFunction = () => { 21 | clearAllListeners(); 22 | device.connected = false; 23 | device.connecting = false; 24 | ble.connectionInFlight = 0; 25 | self.log.info("disconnected", device.id); 26 | device.discovered = false; 27 | device.peripheral = undefined; 28 | 29 | if (self.attachedDiscoverCall === undefined) { 30 | self.attachedDiscoverCall = self.nobleDiscovered.bind(self); 31 | Noble.on("discover", self.attachedDiscoverCall); 32 | } 33 | //scanRestartCount = 0; 34 | self.startScanningWithTimeout(); 35 | self.scanning = true; 36 | }; 37 | 38 | let clearAllListeners = function (){ 39 | if (device.peripheral) { 40 | self.log.debug("Cleaning up Responders for ", device.peripheral.uuid) 41 | device.peripheral.removeAllListeners('connect'); 42 | device.peripheral.removeAllListeners('disconnect'); 43 | device.peripheral.removeAllListeners('servicesDiscover'); 44 | } 45 | if (device.service) { 46 | self.log.debug("Cleaning up Service Responders for ", device.peripheral.uuid) 47 | device.service.removeAllListeners('characteristicsDiscover'); 48 | device.service = undefined; 49 | } 50 | if (device.switchCharacteristic) { 51 | self.log.debug("Cleaning up Characteristic Responders for ", device.peripheral.uuid) 52 | device.switchCharacteristic.unsubscribe((err)=>{}); 53 | device.switchCharacteristic.removeAllListeners('data'); 54 | device.switchCharacteristic = undefined; 55 | } 56 | } 57 | 58 | //when not connected, disconnect and retry... 59 | let timouter = setTimeout(function(){ 60 | timouter = undefined; 61 | self.log.info("Disconnect After Timeout", device.peripheral.uuid) 62 | let per = device.peripheral; 63 | disconnectFunction(); 64 | per.disconnect(); 65 | }, 9000); 66 | 67 | clearAllListeners(); 68 | let rediscoverCount = 0; 69 | if (!device.peripheral.listeners('connect').some(t=>t == device.connectFunction)){ 70 | device.peripheral.on('servicesDiscover', function(services){ 71 | self.log.debug("Discovered Services:", services?services.length:0); 72 | device.service = services?services[0]:undefined; 73 | if (device.service === undefined){ 74 | if (rediscoverCount++ < REDISCOVER_MAX_COUNT) { 75 | self.log.info("Restarting Service Discovery: ct="+rediscoverCount+", id=" + device.peripheral.uuid); 76 | setTimeout(()=>{ device.peripheral.discoverServices([serviceUUID])}, REDISCOVER_TIMEOUT); 77 | } else { 78 | self.log.error("Service Discovery failed. Disconnecting " + device.peripheral.uuid); 79 | device.peripheral.disconnect(); 80 | } 81 | return; 82 | } 83 | rediscoverCount = 0; 84 | self.log.info('Discovered DailySwitch Service on', device.peripheral.uuid); 85 | 86 | device.service.on('characteristicsDiscover', function(characteristics){ 87 | self.log.info('Discovered Characteristics', characteristics?characteristics.length:0); 88 | device.switchCharacteristic = characteristics?characteristics[0]:undefined; 89 | 90 | if (device.switchCharacteristic === undefined){ 91 | if (rediscoverCount++ < REDISCOVER_MAX_COUNT) { 92 | self.log.info("Restarting Characteristic Discovery: ct="+rediscoverCount+", id=" + device.peripheral.uuid); 93 | setTimeout(()=>{device.service.discoverCharacteristics([switchCharacteristicUUID])}, REDISCOVER_TIMEOUT); 94 | } else { 95 | self.log.error("Characteristic Discovery failed. Disconnecting " + device.peripheral.uuid); 96 | device.peripheral.disconnect(); 97 | } 98 | return; 99 | } 100 | 101 | self.log.debug('Discovered DailySwitch Characteristic on', device.peripheral.uuid); 102 | 103 | device.switchCharacteristic.on('data', function(data, isNotification) { 104 | self.log.info('switch sent info: ', data.toString()); 105 | try { 106 | const json = JSON.parse(data.toString()); 107 | self.callback(json); 108 | } catch (e){ 109 | self.log.error(e); 110 | } 111 | }); 112 | 113 | //device.switchCharacteristic.read(); 114 | 115 | // to enable notify 116 | device.switchCharacteristic.subscribe(function(error) { 117 | if (error) { 118 | self.log.error("Subscription Error", error); 119 | } 120 | self.log.debug('Subsribe'); 121 | }); 122 | 123 | //finished connection => make sure watchdog resumes normal operation 124 | ble.connectionInFlight = 0; 125 | }); 126 | 127 | device.service.discoverCharacteristics([switchCharacteristicUUID]); 128 | }); 129 | 130 | //connection handler; 131 | device.connectFunction = (error) => { 132 | if (error){ 133 | self.log.error("Connection Error:", error); 134 | return; 135 | } 136 | 137 | device.connected = true; 138 | self.log.info('connected to peripheral: ' + device.peripheral.uuid); 139 | if (timouter != undefined){ 140 | clearTimeout(timouter); 141 | timouter = undefined; 142 | } 143 | device.peripheral.discoverServices([serviceUUID]); 144 | }; 145 | 146 | device.peripheral.on("connect", device.connectFunction); 147 | device.peripheral.on("disconnect", disconnectFunction); 148 | } else { 149 | ble.log.info("Allready having a connect function => ignoring", device.id); 150 | } 151 | 152 | ble.log.info("Connecting Peripheral"); 153 | ble.log.error("Test Error"); 154 | device.peripheral.connect(); 155 | device.connecting = false; 156 | } 157 | 158 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'), 2 | columnify = require('columnify'); 3 | 4 | TriggerTypes = Object.freeze({"event":1, "time":2, "altitude":3, "lux":4, "calendar":5, "expression":6, "holiday":7}); 5 | TriggerWhen = Object.freeze({"greater":1, "less":-1, "both":0, "match":1, "no-match":-1, "both":0}); 6 | TriggerOps = Object.freeze({"set":0, "and":1, "or":2, 'discard':3}); 7 | EventTypes = Object.freeze({"nightEnd":1, "nauticalDawn":2, "dawn":3, "sunrise":4, "sunriseEnd":5, "goldenHourEnd":6, "solarNoon":7, "goldenHour":8, "sunsetStart":9, "sunset":10, "dusk":11, "nauticalDusk":12, "night":13, "nadir":14}); 8 | 9 | BluetoothSwitchTypes = Object.freeze({"simple":0, "tristate":1, "triggered":2}); 10 | 11 | const CommunicationTypes = Object.freeze({"http":1}); 12 | const WebTypes = Object.freeze({"Generic":0, "DailySensor":1, "DailySocket":2}); 13 | 14 | function triggerOpsName (type){ 15 | switch(type){ 16 | case TriggerOps.set: 17 | return ''; 18 | case TriggerOps.and: 19 | return '[AND]'; 20 | case TriggerOps.or: 21 | return '[OR]'; 22 | case TriggerOps.discard: 23 | return '[DROP]'; 24 | default: 25 | return '[?]'; 26 | } 27 | } 28 | 29 | function triggerEventName (type){ 30 | switch(type){ 31 | case EventTypes.nightEnd: 32 | return 'Night End'; 33 | case EventTypes.nauticalDawn: 34 | return 'Nautical Dawn'; 35 | case EventTypes.dawn: 36 | return 'Dawn'; 37 | case EventTypes.sunrise: 38 | return 'Sunrise'; 39 | case EventTypes.sunriseEnd: 40 | return 'Sunrise End'; 41 | case EventTypes.goldenHourEnd: 42 | return 'Golden Hour End'; 43 | case EventTypes.solarNoon: 44 | return 'Solar Noon'; 45 | case EventTypes.goldenHour: 46 | return 'Golden Hour'; 47 | case EventTypes.sunsetStart: 48 | return 'Sunset Start'; 49 | case EventTypes.sunset: 50 | return 'Sunset'; 51 | case EventTypes.dusk: 52 | return 'Dusk'; 53 | case EventTypes.nauticalDusk: 54 | return 'Nautical Dusk'; 55 | case EventTypes.night: 56 | return 'Night'; 57 | case EventTypes.nadir: 58 | return 'Lowest Sun'; 59 | default: 60 | return 'UNKNOWN'; 61 | } 62 | } 63 | 64 | function triggerTypeName(type, withOp){ 65 | if (withOp === undefined) withOp = false; 66 | var ret = '' 67 | switch(type){ 68 | case TriggerTypes.event: 69 | ret = 'EVENT'; 70 | if (withOp) ret += ' ='; 71 | break; 72 | case TriggerTypes.time: 73 | ret = 'Time'; 74 | if (withOp) ret += ' >'; 75 | break; 76 | case TriggerTypes.altitude: 77 | ret = 'Altitude'; 78 | if (withOp) ret += ' >'; 79 | break; 80 | case TriggerTypes.lux: 81 | ret = 'Lux'; 82 | if (withOp) ret += ' >'; 83 | break; 84 | case TriggerTypes.calendar: 85 | ret = 'Calendar'; 86 | if (withOp) ret += ' ='; 87 | break; 88 | case TriggerTypes.holiday: 89 | ret = 'Holiday'; 90 | if (withOp) ret += ' in'; 91 | break; 92 | case TriggerTypes.expression: 93 | ret = 'Expression'; 94 | if (withOp) ret += ':'; 95 | break; 96 | default: 97 | ret = 'UNKNOWN'; 98 | } 99 | 100 | return ret; 101 | } 102 | 103 | function triggerWhenName(type){ 104 | switch(type){ 105 | case TriggerWhen.greater: 106 | return 'Trigger If'; 107 | case TriggerWhen.less: 108 | return 'Trigger If Not'; 109 | case TriggerWhen.both: 110 | return ''; 111 | default: 112 | return '?'; 113 | } 114 | } 115 | 116 | function dayOfWeekNameList(mask){ 117 | let s = '' 118 | for (var i = 1; i<=7; i++){ 119 | if ((mask & (1<<(i-1))) != 0){ 120 | s+= moment().isoWeekday(i).format('dd')+" " 121 | } 122 | } 123 | s = s.trim(); 124 | if (s!=''){ 125 | s = '(on ' + s + ')'; 126 | } 127 | return s; 128 | } 129 | 130 | function formatTrigger(trigger){ 131 | let s = '' 132 | s += triggerOpsName(trigger.op) + ' ' 133 | s = (s + dayOfWeekNameList(trigger.daysOfWeek)).trim() + ' '; 134 | s = (s + triggerWhenName(trigger.when)).trim() + ' '; 135 | s = (s + triggerTypeName(trigger.type, true)).trim() + ' '; 136 | 137 | switch(trigger.type){ 138 | case TriggerTypes.time: 139 | if (trigger.random && trigger.random!=0) { 140 | s += moment(trigger.randomizedValue).format("LTS"); 141 | s+= ' (' + moment(trigger.value).format("LTS") + '±' + trigger.random + " min.)"; 142 | } else { 143 | s += moment(trigger.value).format("LTS"); 144 | } 145 | 146 | break; 147 | case TriggerTypes.event: 148 | s += triggerEventName(trigger.value); 149 | break; 150 | case TriggerTypes.altitude: 151 | if (trigger.random && trigger.random!=0) { 152 | s += formatRadians(trigger.randomizedValue); 153 | s+= ' (' +formatRadians(trigger.value)+ '±' + formatRadians(trigger.random) + ")"; 154 | } else { 155 | s += formatRadians(trigger.value); 156 | } 157 | break; 158 | case TriggerTypes.lux: 159 | if (trigger.random && trigger.random!=0) { 160 | s += Math.round(trigger.randomizedValue); 161 | s+= ' (' + Math.round(trigger.value) + '±' + trigger.random + ")"; 162 | } else { 163 | s += Math.round(trigger.value); 164 | } 165 | break; 166 | case TriggerTypes.calendar: 167 | s += trigger.value; 168 | break; 169 | case TriggerTypes.holiday: 170 | s += '[' + trigger.value.join(', ') + ']'; 171 | break; 172 | case TriggerTypes.expression: 173 | s += trigger.value.toString(); 174 | break; 175 | default: 176 | s += trigger.value; 177 | } 178 | s += ' (' + trigger.active + ')'; 179 | return s; 180 | } 181 | 182 | function logEvents(events){ 183 | if (events === undefined) return; 184 | const NOW = new Date(); 185 | let printData = []; 186 | events.forEach(function(event){ 187 | printData.push({ 188 | event: event.event, 189 | when: moment(event.when).fromNow(), 190 | time: moment(event.when).format('HH:mm:ss'), 191 | day: moment(event.when).format('ll'), 192 | dif:Math.round((event.when - NOW) / (1000 * 60)), 193 | lux:event.lux, 194 | altitude:event.pos.altitude * 180.0 / Math.PI 195 | }) 196 | }); 197 | console.log(columnify(printData, {minWidth:15})); 198 | } 199 | 200 | function justTime(date){ 201 | if (date===undefined) date=moment(); 202 | 203 | const m = (date instanceof Date) ? moment(date) : date; 204 | return moment({h: m.hours(), m: m.minutes(), s: m.seconds()}); 205 | } 206 | 207 | function formatRadians(rad){ 208 | return formatNumber((rad/Math.PI)*180)+'°'; 209 | } 210 | 211 | function formatNumber(nr){ 212 | return parseFloat(Math.round(nr * 100) / 100).toFixed(2) 213 | } 214 | 215 | 216 | exports.TriggerTypes = TriggerTypes; 217 | exports.TriggerWhen = TriggerWhen; 218 | exports.TriggerOps = TriggerOps; 219 | exports.EventTypes = EventTypes; 220 | exports.BluetoothSwitchTypes = BluetoothSwitchTypes; 221 | exports.CommunicationTypes = CommunicationTypes; 222 | exports.WebTypes = WebTypes; 223 | 224 | exports.triggerOpsName = triggerOpsName; 225 | exports.triggerEventName = triggerEventName; 226 | exports.triggerTypeName = triggerTypeName; 227 | exports.triggerWhenName = triggerWhenName; 228 | exports.dayOfWeekNameList = dayOfWeekNameList; 229 | exports.formatTrigger = formatTrigger; 230 | 231 | exports.logEvents = logEvents; 232 | 233 | exports.justTime = justTime; 234 | exports.formatRadians = formatRadians; 235 | exports.formatNumber = formatNumber; -------------------------------------------------------------------------------- /lib/iCal.js: -------------------------------------------------------------------------------- 1 | const Moment = require('moment'), 2 | MomentRange = require('moment-range'), 3 | moment = MomentRange.extendMoment(Moment), 4 | https = require('https'), 5 | icalExpander = require('ical-expander'), 6 | xmlParser = require('xml-js'); 7 | 8 | exports.loadEventsForDay = function(whenMoment, config, cb) { 9 | const DavTimeFormat = 'YYYYMMDDTHHmms\\Z', 10 | url = config.url, 11 | user = config.username, 12 | pass = config.password, 13 | urlparts = /(https?)\:\/\/(.*?):?(\d*)?(\/.*\/?)/gi.exec(url), 14 | protocol = urlparts[1], 15 | host = urlparts[2], 16 | port = urlparts[3] || (protocol === "https" ? 443 : 80), 17 | path = urlparts[4]; 18 | start = whenMoment.clone().startOf('day'), 19 | end = whenMoment.clone().endOf('day'); 20 | 21 | var xml = '\n' + 22 | '\n' + 23 | ' \n' + 24 | ' \n' + 25 | ' \n' + 26 | ' \n' + 27 | ' \n' + 28 | ' \n' + 29 | ' \n' + 30 | ' \n' + 31 | ' \n' + 32 | ' \n' + 33 | ''; 34 | 35 | var options = { 36 | rejectUnauthorized: false, 37 | hostname: host, 38 | port: port, 39 | path: path, 40 | method: 'REPORT', 41 | headers: { 42 | "Content-type": "application/xml", 43 | "Content-Length": xml.length, 44 | "User-Agent": "calDavClient", 45 | "Connection": "close", 46 | "Depth": "1" 47 | } 48 | }; 49 | 50 | if (user && pass) { 51 | var userpass = Buffer.from(user + ":" + pass).toString('base64'); 52 | options.headers["Authorization"] = "Basic " + userpass; 53 | } 54 | 55 | var req = https.request(options, function (res) { 56 | var s = ""; 57 | res.on('data', function (chunk) { 58 | s += chunk; 59 | }); 60 | 61 | req.on('close', function () { 62 | var reslist = []; 63 | try { 64 | const json = JSON.parse(xmlParser.xml2json(s, {compact: true, spaces: 0})); 65 | 66 | function process(ics){ 67 | const cal = new icalExpander({ ics, maxIterations: 1000 }); 68 | const events = cal.between(start.toDate(), end.toDate()); 69 | 70 | const mappedEvents = events.events.map(e => ({ 71 | startDate: e.startDate.toJSDate(), 72 | endDate: e.endDate.toJSDate(), 73 | range: moment.range(e.startDate.toJSDate(), e.endDate.toJSDate()), 74 | summary: e.summary 75 | })); 76 | const mappedOccurrences = events.occurrences.map(o => ({ 77 | startDate: o.startDate.toJSDate(), 78 | endDate: o.endDate.toJSDate(), 79 | range: moment.range(o.startDate.toJSDate(), o.endDate.toJSDate()), 80 | summary: o.item.summary 81 | })); 82 | reslist = reslist.concat(mappedEvents, mappedOccurrences); 83 | } 84 | 85 | if (json && json.multistatus && json.multistatus.response) { 86 | var ics; 87 | if (json.multistatus.response.propstat) { 88 | process(json.multistatus.response.propstat.prop['calendar-data']._cdata); 89 | } else { 90 | json.multistatus.response.forEach(response => process(response.propstat.prop['calendar-data']._cdata)); 91 | } 92 | } 93 | cb(reslist, start, end); 94 | } catch (e) { 95 | console.error("Error parsing response", e) 96 | } 97 | }); 98 | }); 99 | 100 | req.end(xml); 101 | 102 | req.on('error', function (e) { 103 | console.error('problem with request: ' + e.message); 104 | }); 105 | } 106 | 107 | exports.eventsAt = function(momentWhen, items){ 108 | return items.filter(event => event.range.contains(momentWhen)); 109 | } -------------------------------------------------------------------------------- /lib/mymath.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'), 2 | $ = require('./helpers.js'), 3 | math= require('mathjs');//http://mathjs.org/docs/core/extension.html; 4 | 5 | // create a new data type 6 | function Time(self, when) { 7 | if (when instanceof Time ){ 8 | when = when.value.clone(); 9 | } else if (when instanceof Date){ 10 | when = moment(when); 11 | } else if (typeof when === 'string'){ 12 | when = moment(when, ['h:m a', 'H:m', 'h:m:s a', 'H:m:s', 'YYYY-MM-DD H:m:s', 'YYYY-MM-DD H:m','YYYY-MM-DD', 'YYYY-MM-DD h:m:s a', 'YYYY-MM-DD h:m a']); 13 | } 14 | this.sensor = self; 15 | this.value = when; 16 | } 17 | Time.prototype.isTime = true 18 | Time.prototype.toString = function () { 19 | if (this.value === undefined) return '?'; 20 | return this.value.format('LLLL') 21 | } 22 | Time.prototype.time = function(str){ 23 | if (this.value === undefined) return new Time(this.sensor, undefined); 24 | return new Time(this.sensor, justTime(this.value)); 25 | } 26 | Time.prototype.mo = function(){ 27 | if (this.value === undefined) return false; 28 | return this.value.isoWeekday() == 1; 29 | } 30 | Time.prototype.tu = function(){ 31 | if (this.value === undefined) return false; 32 | return this.value.isoWeekday() == 2; 33 | } 34 | Time.prototype.we = function(){ 35 | if (this.value === undefined) return false; 36 | return this.value.isoWeekday() == 3; 37 | } 38 | Time.prototype.th = function(){ 39 | if (this.value === undefined) return false; 40 | return this.value.isoWeekday() == 4; 41 | } 42 | Time.prototype.fr = function(){ 43 | if (this.value === undefined) return false; 44 | return this.value.isoWeekday() == 5; 45 | } 46 | Time.prototype.sa = function(){ 47 | if (this.value === undefined) return false; 48 | return this.value.isoWeekday() == 6; 49 | } 50 | Time.prototype.so = function(){ 51 | if (this.value === undefined) return false; 52 | return this.value.isoWeekday() == 7; 53 | } 54 | Time.prototype.workday = function(){ 55 | if (this.value === undefined) return false; 56 | return this.value.isoWeekday() < 6; 57 | } 58 | Time.prototype.weekend = function(){ 59 | if (this.value === undefined) return false; 60 | return this.value.isoWeekday() >= 6; 61 | } 62 | Time.prototype.weekday = function(){ 63 | if (this.value === undefined) return false; 64 | return this.value.isoWeekday(); 65 | } 66 | Time.prototype.addMinutes = function(nr){ 67 | if (this.value === undefined) return new Time(this.sensor, undefined); 68 | return new Time(this.sensor, this.value.clone().add(nr, 'minutes')); 69 | } 70 | Time.prototype.addSeconds = function(nr){ 71 | if (this.value === undefined) return new Time(this.sensor, undefined); 72 | return new Time(this.sensor, this.value.clone().add(nr, 'seconds')); 73 | } 74 | Time.prototype.isHoliday = function (types) { 75 | if (this.value === undefined) return false; 76 | return this.sensor.isHoliday(this.value.toDate(), types); 77 | } 78 | Time.prototype.calendarMatch = function(regex){ 79 | if (this.value === undefined) return false; 80 | return Sensor.matchesCalEventNow(this.value.toDate(), regex); 81 | } 82 | Time.prototype.isEvent = function(name){ 83 | if (this.value === undefined) return false; 84 | const event = this.sensor.fetchEventAt(this.value.toDate()); 85 | return $.EventTypes[event.event] == name; 86 | } 87 | 88 | // define a new datatype 89 | math.typed.addType({ 90 | name: 'Time', 91 | test: function (x) { 92 | return x && x.isTime 93 | } 94 | }) 95 | 96 | 97 | // import in math.js, extend the existing function `add` with support for MyType 98 | math.import({ 99 | 'smaller': math.typed('smaller', { 100 | 'Time, Time': function (a, b) { 101 | if (b.value === undefined) return false; 102 | if (a.value === undefined) return true; 103 | return a.value < b.value; 104 | } 105 | }), 106 | 'larger': math.typed('larger', { 107 | 'Time, Time': function (a, b) { 108 | if (a.value === undefined) return false; 109 | if (b.value === undefined) return true; 110 | return a.value > b.value; 111 | } 112 | }), 113 | 'equal': math.typed('equal', { 114 | 'Time, Time': function (a, b) { 115 | if (a.value === undefined) return b.value===undefined; 116 | return a.value.isSame(b.value, 'second'); 117 | } 118 | }), 119 | 'Time': function(self, str){ 120 | return new Time(self, str); 121 | }, 122 | 'dailyrandom': function(self, nr, magnitude){ 123 | if (nr===undefined) nr = 0; 124 | if (magnitude===undefined) magnitude = 1; 125 | 126 | let rnd = self.dailyRandom[nr]; 127 | if (rnd === undefined) { 128 | rnd = Math.random(); 129 | self.dailyRandom[nr] = rnd; 130 | } 131 | 132 | return rnd * magnitude; 133 | }, 134 | 'when':function(condition, val, alt){ 135 | return condition?val:alt; 136 | } 137 | }) 138 | 139 | module.exports = function(sensor){ 140 | const Sensor = sensor; 141 | 142 | return { 143 | Time:Time, 144 | compile : function(exp){ 145 | return { 146 | expression : exp, 147 | code : math.compile(exp), 148 | run : function(constants, when){ 149 | when = new Time(Sensor, when); 150 | 151 | const pos = Sensor.posForTime(when.value.toDate()); 152 | const newLux = Sensor.luxForTime(when.value.toDate(), pos); 153 | let scope = { 154 | ON:0, 155 | OFF:1, 156 | ALT_ON:2, 157 | SINGLE_PRESS:0, 158 | DOUBLE_PRESS:1, 159 | LONG_PRESS:2, 160 | self:Sensor, 161 | altitude:pos.altitude, 162 | altitudeDeg:pos.altitude*180/Math.PI, 163 | azimuth:pos.azimuth, 164 | azimuthDeg:pos.azimuth*180/Math.PI, 165 | lux:newLux, 166 | now:when, 167 | ...constants 168 | } 169 | if (Sensor.bluetooth) { 170 | scope.btSwitch = { 171 | when:new Time(Sensor, Sensor.bluetooth.lastEvent.when), 172 | state:Sensor.bluetooth.lastEvent.state 173 | } 174 | } else { 175 | scope.btSwitch = { 176 | when:new Time(Sensor, new Date()), 177 | state:-1 178 | } 179 | } 180 | 181 | const res = this.code.eval(scope); 182 | if (res.entries) { 183 | return res.entries[0]; 184 | } 185 | return res; 186 | }, 187 | toString : function(){ 188 | return this.expression; 189 | } 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /lib/web.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const moment = require('moment'), 3 | packageJSON = require("../package.json"), 4 | express = require('express'), 5 | bodyParser = require("body-parser"), 6 | path = require('path'), 7 | fs = require('fs'), 8 | $ = require('./helpers.js'); 9 | 10 | var WebServers = {}; 11 | var WebPaths = {}; 12 | var Characteristic; 13 | exports.setCharacteristics = function(c) { 14 | Characteristic = c; 15 | } 16 | 17 | function startServer(acc) { 18 | if (acc.port > 0) { 19 | acc.webType = $.WebTypes.Generic; 20 | const port = acc.port; 21 | acc.log.debug(`Starting HTTP listener on port ${port} for path ${acc.webPath}...`); 22 | 23 | var expressApp = WebServers[acc.port]; 24 | var masterForPort = false; 25 | if (expressApp === undefined) { 26 | masterForPort = true; 27 | expressApp = express(); 28 | expressApp.listen(port, (err) => 29 | { 30 | if (err) { 31 | acc.log.error(`Failed to start Express on port ${port}!`, err); 32 | } else { 33 | acc.log.debug(`Express is running on port ${port}.`) 34 | } 35 | }); 36 | 37 | WebServers[acc.port] = expressApp; 38 | } 39 | 40 | //get list of available paths on a port 41 | var paths = WebPaths[acc.port]; 42 | if (paths === undefined){ 43 | paths = []; 44 | WebPaths[acc.port] = paths; 45 | } 46 | paths.push({ 47 | path:acc.webPath, 48 | name:acc.config.name, 49 | object:acc 50 | }) 51 | 52 | if (masterForPort) { 53 | // parse application/x-www-form-urlencoded 54 | //expressApp.use(bodyParser.urlencoded({ extended: false })); 55 | 56 | // parse application/json 57 | expressApp.use(bodyParser.json()); 58 | 59 | 60 | expressApp.get("/js/d3.js", (request, response) => { 61 | response.sendFile(path.join(__dirname, '../js/d3.v5.min.js')); 62 | }); 63 | expressApp.get("/js/jquery.min.js", (request, response) => { 64 | response.sendFile(path.join(__dirname, '../js/jquery-3.3.1.min.js')); 65 | }); 66 | expressApp.get("/js/bootstrap.min.js", (request, response) => { 67 | response.sendFile(path.join(__dirname, '../js/bootstrap.min.js')); 68 | }); 69 | expressApp.get("/css/bootstrap.min.css", (request, response) => { 70 | response.sendFile(path.join(__dirname, '../css/bootstrap.min.css')); 71 | }); 72 | expressApp.get("/css/style.css", (request, response) => { 73 | response.sendFile(path.join(__dirname, '../css/style.css')); 74 | }); 75 | expressApp.get("/js/bootstrap.min.js.map", (request, response) => { 76 | response.sendFile(path.join(__dirname, '../js/bootstrap.min.js.map')); 77 | }); 78 | 79 | expressApp.get("/css/bootstrap.min.css.map", (request, response) => { 80 | response.sendFile(path.join(__dirname, '../css/bootstrap.min.css.map')); 81 | }); 82 | 83 | expressApp.get("/", (request, response) => { 84 | response.send(buildIndexHTML(acc)); 85 | }); 86 | 87 | expressApp.get("/accessories", (request, response) => { 88 | var json = { 89 | host:request.headers.host, 90 | accessories:[] 91 | } 92 | WebPaths[acc.port].forEach(path => { 93 | json.accessories.push({ 94 | path:path.path, 95 | name:path.name, 96 | type:path.object.webType, 97 | info:JSONCommonResponse(path.object, request) 98 | }); 99 | }); 100 | response.json(json); 101 | }); 102 | } 103 | 104 | expressApp.post(acc.webPath+"/cb", (request, response) => { 105 | const ok = acc.webCallback(request.body); 106 | response.json({ 107 | operation:'cb', 108 | ok:ok, 109 | ...JSONCommonResponse(acc, request) 110 | }); 111 | 112 | acc.log.debug("received cb"); 113 | }); 114 | 115 | expressApp.get(acc.webPath+"/state", (request, response) => { 116 | response.json(JSONCommonResponse(acc, request)); 117 | //acc.log.debug("received STATE"); 118 | }); 119 | 120 | acc.log.info("HTTP listener started on port " + port + " for path " + acc.webPath + "."); 121 | return {expressApp: expressApp, masterForPort:masterForPort}; 122 | } 123 | 124 | return {expressApp: undefined, masterForPort:false}; 125 | } 126 | 127 | exports.startServerForSocket = function(sensor){ 128 | let {expressApp, masterForPort} = startServer(sensor); 129 | if (expressApp) { 130 | sensor.webType = $.WebTypes.DailySocket; 131 | } 132 | } 133 | 134 | exports.startServerForGroup = function(group) { 135 | let {expressApp, masterForPort} = startServer(group); 136 | if (expressApp) { 137 | group.log.info("Listening for " + group.name + " at " + group.webPath + "/event") 138 | expressApp.post(group.webPath+"/event", (request, response) => { 139 | group.log.debug("Got ", request.body); 140 | group.receivedSwitchEvent(request.body); 141 | 142 | response.json({ 143 | operation:'event', 144 | ok:true, 145 | ...JSONCommonResponse(group, request) 146 | }); 147 | group.log.debug("received event on " + group.name); 148 | }); 149 | } 150 | } 151 | 152 | exports.startServerForSensor = function(sensor){ 153 | let {expressApp, masterForPort} = startServer(sensor); 154 | if (expressApp) { 155 | sensor.webType = $.WebTypes.DailySensor; 156 | expressApp.get(sensor.webPath+"/bt/restartDiscover", (request, response) => { 157 | sensor.restartDiscovery(); 158 | response.json({ 159 | operation:'bt/restartDiscover', 160 | ok:true, 161 | ...JSONCommonResponse(sensor, request) 162 | }); 163 | sensor.log.debug("received bt/restartDiscover"); 164 | }); 165 | 166 | expressApp.get(sensor.webPath+"/0", (request, response) => { 167 | sensor.override = 0; 168 | sensor.syncSwitchState(); 169 | response.json({ 170 | operation:'off', 171 | ok:true, 172 | ...JSONCommonResponse(sensor, request) 173 | }); 174 | sensor.log.debug("received OFF"); 175 | }); 176 | expressApp.get(sensor.webPath+"/1", (request, response) => { 177 | sensor.override = 1; 178 | sensor.syncSwitchState(); 179 | response.json({ 180 | operation:'on', 181 | ok:true, 182 | ...JSONCommonResponse(sensor, request) 183 | }); 184 | sensor.log.debug("received ON"); 185 | }); 186 | expressApp.get(sensor.webPath+"/2", (request, response) => { 187 | sensor.override = 2; 188 | sensor.syncSwitchState(); 189 | response.json({ 190 | operation:'long', 191 | ok:true, 192 | ...JSONCommonResponse(sensor, request) 193 | }); 194 | sensor.log.debug("received LONG"); 195 | }); 196 | expressApp.get(sensor.webPath+"/clear", (request, response) => { 197 | sensor.override = undefined; 198 | sensor.syncSwitchState(); 199 | 200 | response.json({ 201 | operation:'clear', 202 | ok:true, 203 | ...JSONCommonResponse(sensor, request) 204 | }); 205 | sensor.log.debug("received CLEAR"); 206 | }); 207 | 208 | expressApp.get(new RegExp(sensor.webPath+"/trigger/(\\d\\d?\\d?)?(/(\\d\\d?))?"), (request, response) => { 209 | const nr = request.params[0]; 210 | if (nr!==undefined) { 211 | if (nr >= 0 && nr {name:$.formatTrigger(t)}) 227 | }); 228 | } 229 | sensor.log.debug("received STATE"); 230 | }); 231 | expressApp.get(sensor.webPath+"/reload", (request, response) => { 232 | sensor.fetchEvents(new Date()); 233 | response.json({ 234 | operation:'reload', 235 | ok:true, 236 | ...JSONCommonResponse(sensor, request) 237 | }); 238 | sensor.log.debug("received STATE"); 239 | }); 240 | expressApp.get(sensor.webPath+"/", (request, response) => { 241 | response.send(buildInfoHTML(sensor)); 242 | }); 243 | } 244 | } 245 | 246 | function JSONCommonResponse(sensor, request) { 247 | let json = { 248 | accessory:{ 249 | name : sensor.config.name, 250 | host : request.headers.host, 251 | path : sensor.webPath, 252 | type : sensor.webType 253 | } 254 | } 255 | if (sensor && sensor.webType && sensor.webType === $.WebTypes.DailySensor){ 256 | const ovr = sensor.override === undefined ? undefined : { 257 | forced:sensor.override, 258 | actual:sensor.isActive 259 | }; 260 | let hbs; 261 | 262 | if (sensor.bluetooth && sensor.bluetooth.type==$.BluetoothSwitchTypes.simple){ 263 | hbs = sensor.currentSwitchValue == Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS ? false : (sensor.currentSwitchValue == Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS ? true : undefined); 264 | } else { 265 | hbs = sensor.currentSwitchValue == Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS ? 0 : (sensor.currentSwitchValue == Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS ? 1 : 2) 266 | } 267 | json = {...json, 268 | bluetoothSwitch : sensor.bluetooth, 269 | activeState:sensor.getIsActive(), 270 | homebridgeState:hbs, 271 | override:ovr, 272 | fixedConfig:sensor.fixedConfig 273 | }; 274 | } else if (sensor && sensor.webType && sensor.webType === $.WebTypes.DailySocket){ 275 | json = {...json, 276 | activeState:sensor.On 277 | }; 278 | } 279 | 280 | return json; 281 | } 282 | 283 | function buildIndexHTML(sensor){ 284 | let s = fs.readFileSync(path.join(__dirname, '../index.html'), { encoding: 'utf8' }); 285 | 286 | s = s.replace('\{\{VERSION\}\}', packageJSON.version); 287 | return s; 288 | } 289 | 290 | function buildInfoHTML(sensor){ 291 | function formatState(state, bold){ 292 | if (bold===undefined) bold=true; 293 | let s = '<'+(bold?'b':'span')+' style="color:'+(state?'green':'red')+'">'; 294 | s += (state?'ON':'OFF'); 295 | s += ''; 296 | return s; 297 | } 298 | const start = moment({h: 0, m: 0, s: 1}) 299 | 300 | const minutes = 1; 301 | let offset = 0; 302 | let p = 0; 303 | 304 | let conditionData = [{data:[], name:'Daystart'}, {data:[], name:'Daystart'}]; 305 | sensor.triggers.forEach(trigger => { 306 | conditionData[2*trigger.id] = { 307 | data:[], 308 | name:$.formatTrigger(trigger) 309 | } 310 | conditionData[2*trigger.id+1] = { 311 | data:[], 312 | name:"Result after " + $.formatTrigger(trigger) 313 | } 314 | }); 315 | 316 | let data = [ 317 | {data:[], min:-1, max:+1, name:'active', blocky:true}, 318 | {data:[], min:-90, max:90, name:'altitude', blocky:false}, 319 | {data:[], min:0, max:100000, name:'lux', blocky:false}]; 320 | 321 | let eventList = {data:[]}; 322 | sensor.events.forEach(event => { 323 | eventList.data.push({ 324 | date:event.when, 325 | name:$.triggerEventName($.EventTypes[event.event]), 326 | value:(180*event.pos.altitude/Math.PI) / 90 327 | }); 328 | }); 329 | 330 | 331 | let tableHTML = ''; 332 | const self = sensor; 333 | const dayStart = sensor.dayStart; 334 | 335 | while (offset < 60*24) { 336 | const mom = start.add(minutes, 'minutes'); 337 | const when = mom.toDate(); 338 | const obj = sensor.testIfActive(when); 339 | 340 | sensor.log.debug(when, obj.active); 341 | 342 | conditionData[0].data[p] = { 343 | date : mom.toDate(), 344 | value : dayStart 345 | }; 346 | conditionData[1].data[p] = { 347 | date : mom.toDate(), 348 | value : dayStart 349 | }; 350 | var all = dayStart; 351 | sensor.triggers.forEach(trigger => { 352 | var item = conditionData[2*trigger.id]; 353 | var itemAll = conditionData[2*trigger.id+1]; 354 | 355 | var one = undefined; 356 | one = self.testTrigger(trigger, when, obj, one, true, true); 357 | all = self.testTrigger(trigger, when, obj, all, false, true); 358 | 359 | item.data[p] = { 360 | date : mom.toDate(), 361 | value : one 362 | }; 363 | itemAll.data[p] = { 364 | date : mom.toDate(), 365 | value : all 366 | }; 367 | }); 368 | 369 | data[0].data[p] = { 370 | date : mom.toDate(), 371 | value : obj.active ? 1 : 0, 372 | time : mom.format('LT'), 373 | values : [obj.active ? 0 : -1, obj.active ? 0 : 1] 374 | }; 375 | data[1].data[p] = { 376 | date : mom.toDate(), 377 | value : 180*obj.pos.altitude/Math.PI, 378 | time : mom.format('LT'), 379 | values : [Math.min(180*obj.pos.altitude/Math.PI, 0), Math.max(180*obj.pos.altitude/Math.PI, 0)] 380 | }; 381 | data[2].data[p] = { 382 | date : mom.toDate(), 383 | value : obj.lux, 384 | time : mom.format('LT'), 385 | values : [Math.min(obj.lux, 0), Math.max(obj.lux, 0)] 386 | }; 387 | offset += minutes; 388 | p++; 389 | 390 | const e = sensor.fetchEventAt(when); 391 | const et = $.triggerEventName(e ? $.EventTypes[e.event] : -1); 392 | tableHTML += ''; 393 | tableHTML += mom.format('LT')+', '; 394 | tableHTML += $.formatRadians(obj.pos.altitude)+', '; 395 | tableHTML += Math.round(obj.lux) +', '; 396 | tableHTML += et + ''; 397 | tableHTML += 'Daystart => '+formatState(dayStart)+'';; 398 | 399 | var last = dayStart; 400 | obj.conditions.forEach(val => { 401 | tableHTML += ''; 402 | tableHTML += $.formatTrigger(val.trigger); 403 | tableHTML += ' => '; 404 | switch (val.trigger.op) { 405 | case $.TriggerOps.and: 406 | tableHTML += formatState(last, false) + ' and ' + formatState(val.active, false) + ' = ' 407 | break; 408 | case $.TriggerOps.or: 409 | tableHTML += formatState(last, false) + ' or ' + formatState(val.active, false) + ' = ' 410 | break; 411 | 412 | case $.TriggerOps.discard: 413 | tableHTML += '[IGNORE]' ; 414 | break; 415 | default: 416 | tableHTML += ''; 417 | } 418 | tableHTML += ''+formatState(val.result, val.trigger.op!=$.TriggerOps.discard)+''; 419 | last = val.result; 420 | }) 421 | 422 | } 423 | 424 | let s = fs.readFileSync(path.join(__dirname, '../template.html'), { encoding: 'utf8' }); 425 | 426 | s = s.replace('\{\{NAME\}\}', sensor.config.name); 427 | s = s.replace('\{\{DATA\}\}', JSON.stringify(data)); 428 | s = s.replace('\{\{TABLE\}\}', tableHTML); 429 | s = s.replace('\{\{EVENT_DATA\}\}', JSON.stringify(eventList)); 430 | s = s.replace('\{\{CONDITION_DATA\}\}', JSON.stringify(conditionData)); 431 | return s; 432 | } 433 | 434 | function triggerRanges(sensor, trigger, triggers, deltaMinutes){ 435 | let begin = moment().startOf('day'); 436 | const result = { 437 | name:$.formatTrigger(trigger), 438 | begin:begin.toISOString(), 439 | interval: deltaMinutes, 440 | dayStart: sensor.dayStart, 441 | trigger:trigger, 442 | ranges:[], 443 | activeStates:[], 444 | } 445 | const triggerAll = { 446 | ...trigger, 447 | when:$.TriggerWhen.both 448 | } 449 | 450 | let active = undefined; 451 | sensor.iterateDay((iterations, time) => { 452 | const when = time.toDate(); 453 | const pos = sensor.posForTime(when); 454 | const newLux = sensor.luxForTime(when, pos); 455 | let obj = { 456 | active:sensor.dayStart, 457 | pos:pos, 458 | lux:newLux, 459 | conditions:[] 460 | }; 461 | 462 | let one = undefined; 463 | one = sensor.testTrigger(trigger, when, obj, one, true, true); 464 | let both = undefined; 465 | both = sensor.testTrigger(triggerAll, when, obj, both, true, true); 466 | let resultObject = { 467 | triggerResult:both, 468 | didTrigger:one!==undefined, 469 | when:time.toISOString(), 470 | ...sensor.testIfActive(when, triggers) 471 | }; 472 | resultObject.activeState = resultObject.active; 473 | resultObject.active = undefined; 474 | resultObject.conditions = [ 475 | {nr:-1, activeState:sensor.dayStart, triggerResult:sensor.dayStart}, 476 | ...resultObject.conditions.map(c => { return {nr:c.trigger.id-1, triggerResult:c.active, activeState:c.result};}) 477 | ]; 478 | result.activeStates.push(resultObject); 479 | 480 | 481 | if (active !== undefined){ 482 | //trigger state changed 483 | if (active != both || iterations==1) { 484 | if (!begin.isSame(time, 'second')) { 485 | const range = { 486 | active:both, 487 | start:begin.toISOString(), 488 | end:time.toISOString(), 489 | willTrigger:one!==undefined 490 | }; 491 | 492 | result.ranges.push(range); 493 | begin = time.clone(); 494 | } 495 | } 496 | } 497 | 498 | active = both; 499 | }, true, deltaMinutes) 500 | 501 | return result; 502 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-daily-sensors", 3 | "version": "0.6.4", 4 | "description": "[alpha] Daytime Sensor Plugin for Homebridge that provides a list of randomizable predefined (and customizable) sensors that trigger at certain daily events (like sunrise), events from your personal calendar, sun elevations, times or lux levels.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "axios": "^0.21.1", 8 | "columnify": "^1.5.4", 9 | "date-holidays": "^1.9.1", 10 | "express": "^4.17.1", 11 | "ical-expander": "^2.1.0", 12 | "mathjs": "^5.10.3", 13 | "minimist": ">=0.2.1", 14 | "mkdirp": "^1.0.4", 15 | "moment": "^2.29.1", 16 | "moment-range": "^4.0.2", 17 | "path": "^0.12.7", 18 | "suncalc": "^1.8.0", 19 | "xml-js": "^1.6.11" 20 | }, 21 | "devDependencies": { 22 | "hap-nodejs": "^0.4.47" 23 | }, 24 | "optionalDependencies": { 25 | "noble": "@abandonware/noble" 26 | }, 27 | "keywords": [ 28 | "homebridge-plugin", 29 | "solar", 30 | "sun", 31 | "daylight", 32 | "sensor", 33 | "time", 34 | "randomized", 35 | "calendar", 36 | "switch", 37 | "alpha" 38 | ], 39 | "repository": { 40 | "type": "git", 41 | "url": "git://github.com/quiqueck/homebridge-daily-sensors" 42 | }, 43 | "bugs": { 44 | "url": "http://github.com/quiqueck/homebridge-daily-sensors/issues" 45 | }, 46 | "engines": { 47 | "node": ">=9.0.0", 48 | "homebridge": ">=0.2.0" 49 | }, 50 | "scripts": { 51 | "start": "node testenv/test.js" 52 | }, 53 | "author": "F. Bauer", 54 | "license": "MIT" 55 | } 56 | -------------------------------------------------------------------------------- /support/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quiqueck/homebridge-daily-sensors/1b5bd35ed19493d80404fdb21ee8f7d15d47e20f/support/index.png -------------------------------------------------------------------------------- /support/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quiqueck/homebridge-daily-sensors/1b5bd35ed19493d80404fdb21ee8f7d15d47e20f/support/info.png -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{NAME}} Info 10 | 11 | 12 | 13 | 14 | 66 | 67 | 68 | 69 | 70 | 71 | {{TABLE}}
72 | 73 | 388 | -------------------------------------------------------------------------------- /testenv/sampleConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "accessory": "DailySensors", 3 | "locale": "de", 4 | "name":"Testzeit", 5 | "webPath":"/testzeit", 6 | "port":8081, 7 | "tickTimer":10000, 8 | "dayStartsActive":false, 9 | "debug":false, 10 | "trigger":[ 11 | { 12 | "active":true, 13 | "type":"time", 14 | "value":"6:15", 15 | "random":20, 16 | "daysOfWeek":[ "mo", "di", "mi", "do", "fr"] 17 | }, 18 | { 19 | "active":false, 20 | "type":"time", 21 | "value":"9:00 PM", 22 | "trigger":"greater", 23 | "random":15 24 | }, 25 | { 26 | "active":false, 27 | "type":"altitude", 28 | "value":"1.4", 29 | "trigger":"both", 30 | "op":"and", 31 | "random":0.5 32 | } 33 | ], 34 | "location":{ 35 | "longitude":-0.118092, 36 | "latitude":51.509865 37 | } 38 | } -------------------------------------------------------------------------------- /testenv/test.js: -------------------------------------------------------------------------------- 1 | const suncalc = require('suncalc'), 2 | moment = require('moment'), 3 | columnify = require('columnify'), 4 | https = require('https'), 5 | fs = require('fs'), 6 | icalExpander = require('ical-expander'), 7 | xmlParser = require('xml-js'), 8 | ical = require('../lib/iCal.js'), 9 | ble = require('../lib/ble.js')({info:console.log, debug:console.log, error:console.error}, ['99b97d90d3e8454881f5489e959bc4f7'], (json)=>{ 10 | console.log("got data ", json); 11 | }); 12 | 13 | const sampleConfig = require('./sampleConfig.json'); 14 | 15 | function nulllog(msg) { 16 | console.log(msg); 17 | }; 18 | 19 | 20 | console.stime = function (date) { 21 | return moment(date).format('LTS \t\t\t ll'); 22 | } 23 | console.time = function (date) { 24 | console.log(console.stime(date)); 25 | } 26 | 27 | const pseudoHomebridge = { 28 | version: "TEST DUMMY", 29 | hap: { 30 | Service: require("hap-nodejs").Service, 31 | Characteristic: require("hap-nodejs").Characteristic, 32 | uuid: 0 33 | }, 34 | platformAccessory: [], 35 | registerPlatform: function (pluginName, platformName, constructor, dynamic) { 36 | const obj = new constructor(nulllog, sampleConfig); 37 | 38 | let events = obj.eventsForDate(new Date(), false); 39 | console.log("TODAY's EVENT LIST"); 40 | console.logEvents(obj.events); 41 | console.log("CURRENT") 42 | console.log(obj.currentEvent, obj.posForTime(new Date()).altitude * 180 / Math.PI); 43 | 44 | obj.updateState(); 45 | }, 46 | registerAccessory: function (pluginName, platformName, constructor, dynamic) { 47 | const obj = new constructor(nulllog, sampleConfig); 48 | 49 | let events = obj.eventsForDate(new Date(), false); 50 | console.log("TODAY's EVENT LIST"); 51 | console.logEvents(obj.events); 52 | console.log("CURRENT") 53 | console.log(obj.currentEvent, obj.posForTime(new Date()).altitude * 180 / Math.PI); 54 | 55 | obj.updateState(); 56 | }, 57 | 58 | }; 59 | //const plugin = require('../index.js')(pseudoHomebridge); 60 | 61 | /*const username = "", 62 | password = "", 63 | url = ""; 64 | 65 | ical.loadEventsForDay(moment(), {url:url, username:username, password:password, type:'caldav'}, (list, start, end) => { 66 | //console.log(list); 67 | ical.eventsAt(moment(), list).map(e => console.log(e)); 68 | });*/ 69 | 70 | //const code = mymath.compile('events.isHoliday(Time("2018-05-31 16:30:23"))'); 71 | const sensor = { 72 | dailyRandom:[], 73 | config: { 74 | location:{ 75 | country:'DE', 76 | state:'BY' 77 | } 78 | }, 79 | bluetooth:{lastEvent:{when:new Date()}}, 80 | posForTime:function(a) { return {altitude:1.2}; }, 81 | luxForTime:function(a, b) { return 2; }, 82 | matchesCalEventNow:function(a, b) { return false; }, 83 | fetchEventAt:function(a) { return false; }, 84 | } 85 | 86 | const s2 = { 87 | dailyRandom:[], 88 | config: { 89 | location:{ 90 | country:'DE', 91 | state:'BY' 92 | } 93 | }, 94 | posForTime:function(a) { return {altitude:1.2}; }, 95 | luxForTime:function(a, b) { return 2; }, 96 | matchesCalEventNow:function(a, b) { return false; }, 97 | fetchEventAt:function(a) { return false; }, 98 | } 99 | const mymath = new (require('../lib/mymath.js'))(sensor) 100 | const mymath2 = new (require('../lib/mymath.js'))(s2) 101 | //const code = mymath.compile("t = Time(self, '6:30 am').addMinutes(20);t>now"); 102 | const code = mymath.compile("dailyrandom(self,2, 10)"); 103 | const code2 = mymath2.compile('when(dailyrandom(self,2, 10)>5, ON, OFF)'); 104 | const scope = { 105 | a : new mymath.Time(sensor, '23:30'), 106 | b : new mymath.Time(s2, '17:30') 107 | } 108 | const now = moment(); 109 | console.log('a:', scope.b.toString()) 110 | console.log('b:', scope.b.toString()) 111 | console.log('now:', now.format('LLLL')) 112 | console.log(code.run(scope, now)); 113 | console.log(typeof(code.run(scope, now))); 114 | console.log(code2.run(scope, now)); 115 | 116 | console.log("Started"); --------------------------------------------------------------------------------