homebridge-virtual-accessories
Version:
Virtual HomeKit accessories for Homebridge.
200 lines • 9.42 kB
JavaScript
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