diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..1e285ad --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "airbnb-base" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..960be9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +package-lock.json +yarn.lock \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5a3a2a --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Automation - Calendar + +Example config.json: + +``` + "accessories": [ + { + "accessory": "AutomationCalendar", + "name": "Test", + "latitude": 51.4825747, + "longitude": -0.0251685 + } + ] + +``` + +This accessory will create a fake sensor with some custom characteristics. +Use one or more characteristic to limit the automation to a specific period of time (e.g. only during the winter or only during the day). + +## Configuration + +* `latitude` your home latitude (used for astronomical calculations); e.g. *-33.8567844* +* `longitude` your home longitude (used for astronomical calculations); e.g. *151.2152967* + +## Characteristics exposed + +### Month of the year +The current month (1 is January, 2 is February, ..., 12 is December). + +### Week of the year +The week of the year, according to the Node.js locale. See [moment.js documentation](https://momentjs.com/docs/#/get-set/week/) for more info. + +### Season +The current astronomical season, based on the latitude/longitude provided. + +| Season | Enum Value | +| --------- | ---------- | +| Spring | 1 | +| Summer | 2 | +| Autumn | 3 | +| Winter | 4 | + +### Season name +The label of the current season (it cannot be used for automation, use the **Season** characteristic instead). + +### Time of the day +The time of the day, based on the latitude/longitude provided. + +| Time of the day | Enum Value | +| ----------------- | ---------- | +| Morning Twilight | 1 | +| Sunrise | 2 | +| Daytime | 3 | +| Sunset | 4 | +| Evening Twilight | 5 | +| Nighttime | 6 | + +### Time of the day label +The label for the current type of the day (it cannot be used for automation, use the **Time of the day** characteristic instead). diff --git a/custom-characteristics.js b/custom-characteristics.js new file mode 100644 index 0000000..f31418b --- /dev/null +++ b/custom-characteristics.js @@ -0,0 +1,106 @@ +const { Characteristic } = require('hap-nodejs'); + +const MonthOfYearUUID = '3470e956-0bfd-11e8-ba89-0ed5f89f718b'; +const MonthOfYear = function () { + const char = new Characteristic('Month of the year', MonthOfYearUUID); + + char.setProps({ + format: Characteristic.Formats.UINT8, + maxValue: 12, + minValue: 1, + minStep: 1, + perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY], + }); + char.value = char.getDefaultValue(); + + return char; +}; +MonthOfYear.UUID = MonthOfYearUUID; + +const WeekOfYearUUID = '3af64fbe-0bfd-11e8-ba89-0ed5f89f718b'; +const WeekOfYear = function () { + const char = new Characteristic('Week of the year', WeekOfYearUUID); + + char.setProps({ + format: Characteristic.Formats.UINT8, + maxValue: 52, + minValue: 1, + minStep: 1, + perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY], + }); + char.value = char.getDefaultValue(); + + return char; +}; +WeekOfYear.UUID = WeekOfYearUUID; + +const SeasonUUID = '9469f834-0c07-11e8-ba89-0ed5f89f718b'; +const Season = function () { + const char = new Characteristic('Season', SeasonUUID); + + char.setProps({ + format: Characteristic.Formats.UINT8, + maxValue: 4, + minValue: 1, + minStep: 1, + perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY], + }); + char.value = char.getDefaultValue(); + + return char; +}; +Season.UUID = SeasonUUID; + +const SeasonNameUUID = '8fb61458-0c07-11e8-ba89-0ed5f89f718b'; +const SeasonName = function () { + const char = new Characteristic('Season name', SeasonNameUUID); + + char.setProps({ + format: Characteristic.Formats.STRING, + perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY], + }); + char.value = char.getDefaultValue(); + + return char; +}; +SeasonName.UUID = SeasonNameUUID; + +const TimeOfDayUUID = 'b6be5238-0c0a-11e8-ba89-0ed5f89f718b'; +const TimeOfDay = function () { + const char = new Characteristic('Time of the day', TimeOfDayUUID); + + char.setProps({ + format: Characteristic.Formats.UINT8, + maxValue: 6, + minValue: 1, + minStep: 1, + perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY], + }); + char.value = char.getDefaultValue(); + + return char; +}; +TimeOfDay.UUID = TimeOfDayUUID; + +const TimeOfDayNameUUID = 'a9d6a962-0c0a-11e8-ba89-0ed5f89f718b'; +const TimeOfDayName = function () { + const char = new Characteristic('Time of the day label', TimeOfDayNameUUID); + + char.setProps({ + format: Characteristic.Formats.STRING, + perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY], + }); + char.value = char.getDefaultValue(); + + return char; +}; +TimeOfDayName.UUID = TimeOfDayNameUUID; + +module.exports = { + MonthOfYear, + WeekOfYear, + Season, + SeasonName, + TimeOfDay, + TimeOfDayName, +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..17c7d4d --- /dev/null +++ b/index.js @@ -0,0 +1,133 @@ +const moment = require('moment'); +const debug = require('debug')('homebridge-automation-calendar'); +const seasonCalculator = require('date-season'); +const CustomCharacteristics = require('./custom-characteristics'); +const timeOfDayCalculator = require('./time-of-day'); + +let Service; + +class AutomationCalendar { + constructor(log, config) { + this.log = log; + this.name = config.name; + + this.latitude = config.latitude || 0.0; + this.longitude = config.longitude || 0.0; + + this.hemisphere = config.latitude >= 0 ? 'north' : 'south'; + + debug(`Using astronomic calendar to get current season - ${this.hemisphere} hemisphere`); + + this.motionService = new Service.MotionSensor(this.name); + + this.motionService + .addCharacteristic(CustomCharacteristics.MonthOfYear) + .on('get', callback => callback(null, this.constructor.getMonthOfYear())); + + this.motionService + .addCharacteristic(CustomCharacteristics.WeekOfYear) + .on('get', callback => callback(null, this.constructor.getWeekOfYear())); + + this.motionService + .addCharacteristic(CustomCharacteristics.Season) + .on('get', callback => callback(null, this.getSeason())); + + this.motionService + .addCharacteristic(CustomCharacteristics.SeasonName) + .on('get', callback => callback(null, this.getSeasonName())); + + this.motionService + .addCharacteristic(CustomCharacteristics.TimeOfDay) + .on('get', callback => callback(null, this.getTimeOfDay())); + + this.motionService + .addCharacteristic(CustomCharacteristics.TimeOfDayName) + .on('get', callback => callback(null, this.getTimeOfDayName())); + + this.refreshValues(); + } + + getServices() { + return [this.motionService]; + } + + static getMonthOfYear() { + return (new Date()).getMonth() + 1; + } + + static getWeekOfYear() { + return moment().week(); + } + + getSeason() { + const seasons = { + Spring: 1, + Summer: 2, + Autumn: 3, + Winter: 4, + }; + + return seasons[this.getSeasonName()]; + } + + getSeasonName() { + const seasonResolver = seasonCalculator({ north: this.hemisphere === 'north', autumn: true }); + + return seasonResolver(new Date()); + } + + getTimeOfDay() { + return timeOfDayCalculator(this.latitude, this.longitude); + } + + getTimeOfDayName() { + const names = { + 0: 'Night', + 1: 'Morning Twilight', + 2: 'Sunrise', + 3: 'Daytime', + 4: 'Sunset', + 5: 'Evening Twilight', + }; + + return names[this.getTimeOfDay()]; + } + + refreshValues() { + this.motionService + .getCharacteristic(CustomCharacteristics.MonthOfYear) + .updateValue(this.constructor.getMonthOfYear()); + + this.motionService + .getCharacteristic(CustomCharacteristics.WeekOfYear) + .updateValue(this.constructor.getWeekOfYear()); + + this.motionService + .getCharacteristic(CustomCharacteristics.Season) + .updateValue(this.getSeason()); + + this.motionService + .getCharacteristic(CustomCharacteristics.SeasonName) + .updateValue(this.getSeasonName()); + + this.motionService + .getCharacteristic(CustomCharacteristics.TimeOfDay) + .updateValue(this.getTimeOfDay()); + + this.motionService + .getCharacteristic(CustomCharacteristics.TimeOfDayName) + .updateValue(this.getTimeOfDayName()); + + // Set timeout + setTimeout( + this.refreshValues.bind(this), + 60000, + ); + } +} + +module.exports = (homebridge) => { + Service = homebridge.hap.Service; // eslint-disable-line + + homebridge.registerAccessory('homebridge-automation-calendar', 'AutomationCalendar', AutomationCalendar); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..1ed70cb --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "homebridge-automation-calendar", + "version": "0.0.1", + "description": "Useful calendar information for HomeKit automations (seasons, week of the year, month of the year, morning twilight/sunrise/daytime/sunset/evening twilight/night)", + "license": "ISC", + "keywords": [ + "homebridge-plugin" + ], + "author": { + "name": "Paolo Tremadio" + }, + "repository": { + "type": "git", + "url": "git://github.com/paolotremadio/homebridge-automation-calendar.git" + }, + "bugs": { + "url": "http://github.com/paolotremadio/homebridge-automation-calendar/issues" + }, + "engines": { + "node": ">=6.0.0", + "homebridge": ">=0.4.0" + }, + "dependencies": { + "date-season": "^0.0.2", + "debug": "^3.1.0", + "hap-nodejs": "^0.4.28", + "moment": "^2.20.1", + "suncalc": "^1.8.0" + }, + "devDependencies": { + "eslint": "^4.16.0", + "eslint-config-airbnb-base": "^12.1.0", + "eslint-plugin-import": "^2.8.0" + }, + "scripts": { + "lint": "./node_modules/eslint/bin/eslint.js . --ext .js" + } +} diff --git a/time-of-day.js b/time-of-day.js new file mode 100644 index 0000000..d1564d8 --- /dev/null +++ b/time-of-day.js @@ -0,0 +1,47 @@ +const suncalc = require('suncalc'); + +// Thanks to https://github.com/kcharwood/homebridge-suncalc/blob/master/index.js +const timeOfDay = (latitude, longitude) => { + const nowDate = new Date(); + const now = nowDate.getTime(); + + const sunDates = suncalc.getTimes( + nowDate, + latitude, + longitude, + ); + + const times = { + dawn: sunDates.dawn.getTime(), + sunrise: sunDates.sunrise.getTime(), + sunriseEnd: sunDates.sunriseEnd.getTime() + (1000 * 60), + sunsetStart: sunDates.sunsetStart.getTime() + (1000 * 60), + sunset: sunDates.sunset.getTime(), + dusk: sunDates.dusk.getTime(), + }; + + if (now < times.dawn) { + // Nighttime + return 6; + } else if (now >= times.dawn && now < times.sunrise) { + // Morning Twilight + return 1; + } else if (now >= times.sunrise && now < times.sunriseEnd) { + // Sunrise + return 2; + } else if (now >= times.sunriseEnd && now < times.sunsetStart) { + // Daytime + return 3; + } else if (now >= times.sunsetStart && now < times.sunset) { + // Sunset + return 4; + } else if (now >= times.sunset && now < times.dusk) { + // Evening Twilight + return 5; + } + + // Nighttime + return 6; +}; + +module.exports = timeOfDay;