UNPKG

homebridge-virtual-accessories

Version:
200 lines 9.42 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; import { Sensor } from '../sensor.js'; import { Trigger } from './trigger.js'; import { Utils } from '../../utils/utils.js'; import { Cron } from 'croner'; import { DateTimeFormatter, LocalDate, LocalDateTime } from '@js-joda/core'; import { Type, deserialize } from 'typeserializer'; import 'reflect-metadata'; /** * SunEventsTrigger - Trigger implementation */ export class SunEventsTrigger extends Trigger { SunTimesURL = (latitude, longitude, timezone, date) => `https://api.sunrisesunset.io/json?time_format=24&lat=${latitude}&lng=${longitude}&timezone=${timezone}&date=${date}`; triggerCronJob; dataCronJob; timezone; constructor(sensor, name) { super(sensor, name); const triggerConfig = this.sensorConfig.sunEventsTrigger; if (triggerConfig.isDisabled) { this.log.info(`[${this.sensorConfig.accessoryName}] Sun Events trigger is disabled`); return; } const timezone = (triggerConfig.zoneId !== undefined) ? triggerConfig.zoneId : Intl.DateTimeFormat().resolvedOptions().timeZone; this.setupSunEvent(triggerConfig, timezone, sensor); // Data retrieval cron job const pattern = '1 0 * * *'; // Every day at 00:01 - one minute after midnight this.log.debug(`[${this.sensorConfig.accessoryName}] Creating data cron job: pattern ${pattern}; timezone ${timezone}`); this.dataCronJob = new Cron(pattern, { name: `Fetch Sun Data Cron Job (${this.sensorConfig.accessoryName})`, timezone: timezone, }, (async () => { this.log.debug(`[${this.sensorConfig.accessoryName}] Matched data cron job pattern '${pattern}'. Triggering sensor`); this.setupSunEvent(triggerConfig, timezone, sensor); this.displayNextRun(this.dataCronJob); })); this.displayNextRun(this.dataCronJob); } displayNextRun(cronJob) { const nextRun = cronJob.nextRun(); const nextRunTimestamp = (nextRun === null) ? 'None scheduled' : `${nextRun.toString().split('.')[0]}; max count: ${cronJob.options.maxRuns}`; this.log.debug(`[${this.sensorConfig.accessoryName}] Next "${cronJob.name}" run: ${nextRunTimestamp}`); } async setupSunEvent(triggerConfig, timezone, sensor) { const today = LocalDate.now().toString(); this.log.debug(`[${this.sensorConfig.accessoryName}] Today: ${today}`); await this.getSunEventsData(triggerConfig.latitude, triggerConfig.longitude, timezone, today) .then((async (response) => { if (response !== undefined) { if (response.status !== SunEventsResponse.OK) { this.log.error(`[${this.sensorConfig.accessoryName}] Sunrise/sunset server returned error response: ${response.status}`); } else { await this.setupTriggerCron(triggerConfig.event, triggerConfig.offset, response.results, sensor); } } })); } async getSunEventsData(latitude, longitude, timezone, date) { let response; const request = new Request(this.SunTimesURL(latitude, longitude, timezone, date), { method: 'GET' }); this.log.debug(`[${this.sensorConfig.accessoryName}] Requesting sunrise/sunset data from: ${(request.url)}`); const maxAttempts = 5; const millis = 60 * 1000; const waitMinutes = 2; // Attempts over 30 minutes // Backoff / retry schedule: // 1 * 2 = 2 mins / :00 + 2 mins = :02 // 2 * 2 = 4 mins / :02 + 4 mins = :06 // 3 * 2 = 6 mins / :06 + 6 mins = :12 // 4 * 2 = 8 mins / :12 + 8 mins = :20 // 5 * 2 = 10 mins / :20 + 10 mins = :30 let dataResponse; let gaveUp = false; let attempts = 0; let dataFetchResponse; do { try { dataFetchResponse = await fetch(request); } catch (error) { this.log.error(`[${this.sensorConfig.accessoryName}] Failed getting sunrise/sunset data: ${JSON.stringify(error)}`); } if (dataFetchResponse === undefined || !dataFetchResponse.ok) { this.log.error(`[${this.sensorConfig.accessoryName}] Error fetching sunrise/sunset data. Response status: ${dataFetchResponse?.status}`); attempts++; const baseErrorMsg = `Failed ${attempts} of ${maxAttempts} attempts.`; if (attempts === maxAttempts) { gaveUp = true; this.log.error(`[${this.sensorConfig.accessoryName}] ${baseErrorMsg} Giving up`); } else { const backoffMinutes = (attempts * waitMinutes); this.log.error(`[${this.sensorConfig.accessoryName}] ${baseErrorMsg} Waiting ${backoffMinutes} minutes until next attempt`); await new Promise(resolve => setTimeout(resolve, backoffMinutes * millis)); } } } while ((dataFetchResponse === undefined || !dataFetchResponse.ok) && attempts < maxAttempts); if (!gaveUp) { dataResponse = await dataFetchResponse.text(); this.log.debug(`[${this.sensorConfig.accessoryName}] Fetched sunrise/sunset data: ${(dataResponse)}`); response = this.desrializeSunEventsResponse(dataResponse); } return response; } desrializeSunEventsResponse(dataResponse) { let response; try { response = deserialize(dataResponse, SunEventsResponse); } catch (error) { this.log.error(`[${this.sensorConfig.accessoryName}] Error deserializing response data: ${JSON.stringify(error)}`); this.log.debug(`[${this.sensorConfig.accessoryName}] Response data: ${response}`); } return response; } async setupTriggerCron(event, offset, dailyDetails, sensor) { let eventTime; switch (event) { case 'sunrise': eventTime = dailyDetails.sunrise; break; case 'sunset': eventTime = dailyDetails.sunset; break; case 'goldenhour': eventTime = dailyDetails.golden_hour; break; default: this.log.error(`[${this.sensorConfig.accessoryName}] Error creating sunrise/sunset trigger. Invalid event: ${event}`); return; } let cronRunTimestamp = `${dailyDetails.date}T${eventTime}`; this.log.debug(`[${this.sensorConfig.accessoryName}] Cron run timestamp: ${cronRunTimestamp}; offset: ${offset} minutes`); if (offset !== 0) { cronRunTimestamp = this.addOffset(cronRunTimestamp, offset); } const runTimezone = dailyDetails.timezone; this.log.debug(`[${this.sensorConfig.accessoryName}] Creating cron for: event ${event}; timestamp: ${cronRunTimestamp}; timezone: ${runTimezone}`); // Hardcode reset delay const resetDelayMillis = 3 * 1000; // 3 second reset delay // Just in case if (this.triggerCronJob !== undefined) { this.triggerCronJob.stop(); } this.triggerCronJob = new Cron(cronRunTimestamp, { name: `Sun Events Cron Job (${this.sensorConfig.accessoryName})`, maxRuns: 1, timezone: runTimezone, }, (async () => { const now = Utils.now().toString(); this.log.debug(`[${this.sensorConfig.accessoryName}] Now ${now} matched event time '${cronRunTimestamp}'. Triggering sensor`); sensor.triggerSensorState(Sensor.TRIGGERED, this); await this.delay(resetDelayMillis); sensor.triggerSensorState(Sensor.NORMAL, this); })); this.displayNextRun(this.triggerCronJob); } delay(millis) { return new Promise(resolve => setTimeout(resolve, millis)); } addOffset(datetime, offset) { const offsetDateTime = LocalDateTime.parse(datetime).plusMinutes(offset).format(DateTimeFormatter.ISO_DATE_TIME); return offsetDateTime; } } class DailyDetails { date; sunrise; sunset; first_light; last_light; dawn; dusk; solar_noon; golden_hour; day_length; timezone; utc_offset; } class SunEventsResponse { static OK = 'OK'; results; status; } __decorate([ Type(DailyDetails), __metadata("design:type", DailyDetails) ], SunEventsResponse.prototype, "results", void 0); //# sourceMappingURL=triggerSunEvents.js.map