@vandubois/homebridge-virtual-switch
Version:
Creation of virtual switches, triggered independently and by keywords appearing in the Homebridge log file.
239 lines • 11.6 kB
JavaScript
export class HomebridgeVirtualSwitchesAccessory {
platform;
accessory;
service;
switchState = false;
timer;
timerEndTime;
useLogFile;
constructor(platform, accessory) {
this.platform = platform;
this.accessory = accessory;
this.useLogFile = accessory.context.device.UseLogFile;
// Set up accessory information
this.setupAccessoryInformation();
// Attempt to initialize the switch service
this.initializeService();
// Initialize state
this.initializeState();
}
initializeService() {
// Assign `service` outside of try-catch for safer error handling
this.service = this.accessory.getService(this.platform.Service.Switch) ||
this.accessory.addService(this.platform.Service.Switch);
try {
// Only wrap code that might throw an error in try-catch
this.service.setCharacteristic(this.platform.Characteristic.Name, this.accessory.context.device.Name);
this.service.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setOn.bind(this))
.onGet(this.getOn.bind(this));
}
catch (error) {
if (error instanceof Error) {
this.platform.log.error(`Failed to initialize switch characteristics: ${error.message}`);
}
else {
this.platform.log.error('Failed to initialize switch characteristics: Unknown error occurred');
}
}
}
setupAccessoryInformation() {
const accessoryInformationService = this.accessory.getService(this.platform.Service.AccessoryInformation);
if (accessoryInformationService) {
accessoryInformationService
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'HomebridgeVirtualSwitchesPlatform')
.setCharacteristic(this.platform.Characteristic.Model, 'HomebridgeVirtualSwitchAccessory-Switch')
.setCharacteristic(this.platform.Characteristic.Name, this.accessory.context.device.Name)
.setCharacteristic(this.platform.Characteristic.SerialNumber, `HomebridgeVirtualSwitchAccessory-${this.accessory.context.device.Name}`)
.setCharacteristic(this.platform.Characteristic.FirmwareRevision, '0.0.0');
}
}
initializeState() {
const device = this.accessory.context.device;
// Check if UseCustomTime is selected with all custom time fields as 0
const hasZeroCustomTime = device.TimeDays === 0 &&
device.TimeHours === 0 &&
device.TimeMinutes === 0 &&
device.TimeSeconds === 0;
if (!device.SwitchStayOn && device.UseCustomTime && hasZeroCustomTime) {
throw new Error(`Switch "${device.Name}" cannot be initialized: "Set timer in days/hours/..." is selected and all time fields are 0 in switch config.`);
}
// Check if Time is 0 when UseCustomTime is not selected
if (!device.SwitchStayOn && !device.UseCustomTime && device.Time === 0) {
this.platform.log.info(`SwitchStayon "${device.SwitchStayOn}"`);
this.platform.log.info(`UseCustomTime: "${device.UseCustomTime}"`);
throw new Error(`Switch "${device.Name}" cannot be initialized:"Timer (in ms)" is 0 and "Set timer in day/hours/..." is not selected in switch config.`);
}
// Check for persistent timer state
if (device.TimerPersistent && device.timerState) {
const { targetTime, isRunning } = device.timerState;
if (isRunning) {
if (this.platform.hasReachedTargetTime(targetTime)) {
// Timer has expired while system was off
this.switchState = device.NormallyClosed;
this.platform.clearTimerState(device.Name);
this.platform.log.info(`Switch "${device.Name}" turned ${device.NormallyClosed ? 'on' : 'off'} as persistent timer expired during downtime.`);
}
else {
// Resume timer and set correct state
const remainingTime = targetTime - Date.now();
this.switchState = !device.NormallyClosed; // Set to triggered state
this.startOffTimer({
targetTime: targetTime,
duration: remainingTime,
});
this.platform.log.info(`Resuming persistent timer for "${device.Name}" with ${remainingTime}ms remaining. Switch state set to ${this.switchState ? 'on' : 'off'}.`);
}
}
// Initialize switch state
}
else if (device.RememberState && device.lastState !== null) {
this.switchState = device.lastState;
}
else {
this.switchState = device.NormallyClosed;
}
this.updateHomeKitState();
const homeKitState = this.switchState;
this.platform.log.info(`Switch "${device.Name}" initialized as ${homeKitState ? 'on' : 'off'} ` +
`(Normally ${device.NormallyClosed ? 'Closed' : 'Open'})`);
}
async setOn(value) {
const device = this.accessory.context.device;
const newHomeKitState = value;
const isOneShotTimer = device.OneShotTimer;
if (!device.NormallyClosed) {
this.switchState = device.NormallyClosed ? !newHomeKitState : newHomeKitState;
}
else {
this.switchState = newHomeKitState;
}
if (this.switchState !== device.NormallyClosed) {
//this.platform.log.info(`Switch "${device.Name}" turned ${device.NormallyClosed ? 'off' : 'on'}.`);
if (!device.SwitchStayOn) {
// Only start timer if it's not running or if OneShotTimer is false
if (!isOneShotTimer || !this.timer || !this.timerEndTime || Date.now() >= this.timerEndTime) {
const timerInfo = this.platform.calculateTargetTime(device);
this.startOffTimer(timerInfo);
}
else {
this.platform.log.debug(`DEBUG: "${device.Name}" Ignoring timer restart (OneShot timer is active)`);
}
}
else {
this.platform.log.info(`Switch "${device.Name}" turned ${device.NormallyClosed ? 'off' : 'on'}.`);
}
}
else {
if (device.SwitchStayOn) {
this.platform.log.info(`Switch "${device.Name}" turned ${device.NormallyClosed ? 'on' : 'off'}.`);
}
this.clearTimer();
}
if (device.RememberState) {
this.platform.saveSwitchState(device.Name, this.switchState);
}
}
async getOn() {
return this.switchState;
}
triggerSwitch() {
const device = this.accessory.context.device;
const isTriggered = this.switchState !== device.NormallyClosed;
const isOneShotTimer = device.OneShotTimer;
// Handle stateful switches with log file monitoring
if (isTriggered && device.SwitchStayOn && this.useLogFile) {
this.platform.log.debug(`DEBUG: "${device.Name}" Ignoring trigger due to keyword (stateful switch).`);
return;
}
// Handle non-stateful switches with active timers
if (isTriggered && !device.SwitchStayOn && this.timer && this.timerEndTime && Date.now() < this.timerEndTime) {
if (isOneShotTimer) {
this.platform.log.debug(`DEBUG: "${device.Name}" Ignoring trigger as the timer is still active.`);
return;
}
// If not OneShot timer, continue with normal operation (timer will restart)
}
// Toggle switch state
this.switchState = !this.switchState;
this.updateHomeKitState();
if (this.switchState !== device.NormallyClosed) {
//this.platform.log.info(`Switch "${device.Name}" turned ${device.NormallyClosed ? 'off' : 'on'}.`);
if (!device.SwitchStayOn) {
// Only start timer if it's not running or if OneShotTimer is false
if (!isOneShotTimer || !this.timer || !this.timerEndTime || Date.now() >= this.timerEndTime) {
if (device.TimerPersistent) {
const timerInfo = this.platform.calculateTargetTime(device);
this.startOffTimer(timerInfo);
}
else {
this.startOffTimer({ targetTime: 0, duration: device.Time });
}
}
else {
this.platform.log.debug(`DEBUG: "${device.Name}" Keeping existing timer (OneShot timer is active)`);
}
}
else {
this.platform.log.info(`Switch "${device.Name}" turned ${device.NormallyClosed ? 'off' : 'on'}.`);
}
}
else {
if (device.SwitchStayOn) {
this.platform.log.info(`Switch "${device.Name}" turned ${device.NormallyClosed ? 'on' : 'off'}.`);
}
this.clearTimer();
}
// Save state if required
if (device.RememberState) {
this.platform.saveSwitchState(device.Name, this.switchState);
}
}
startOffTimer(timerInfo) {
const device = this.accessory.context.device;
this.clearTimer();
this.timerEndTime = timerInfo.targetTime || (Date.now() + timerInfo.duration);
// Log timer information
const status = device.NormallyClosed ? 'off' : 'on';
const futureStatus = device.NormallyClosed ? 'on' : 'off';
if (device.TimerPersistent) {
this.platform.log.info(`Switch "${device.Name}" turned ${status} and will turn ${futureStatus} at ${new Date(this.timerEndTime).toLocaleString()}`);
// Save persistent timer state
this.platform.saveTimerState(device.Name, this.timerEndTime, true);
}
else {
this.platform.log.info(`Switch "${device.Name}" turned ${status} and will turn ${futureStatus} after ${timerInfo.duration} milliseconds.`);
}
this.timer = setTimeout(() => {
this.switchState = device.NormallyClosed;
this.updateHomeKitState();
if (device.TimerPersistent) {
this.platform.clearTimerState(device.Name);
}
const homeKitState = this.switchState;
this.platform.log.info(`Switch "${device.Name}" turned ${homeKitState ? 'on' : 'off'} automatically after timer expired.`);
}, timerInfo.duration);
}
clearTimer() {
const device = this.accessory.context.device;
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
this.timerEndTime = undefined;
if (device.TimerPersistent) {
this.platform.clearTimerState(device.Name);
}
}
}
updateHomeKitState() {
const homeKitState = this.switchState;
const service = this.accessory.getService(this.platform.Service.Switch);
if (service) {
service.updateCharacteristic(this.platform.Characteristic.On, homeKitState);
}
else {
this.platform.log.error(`ERROR: Failed to find the switch service for "${this.accessory.displayName}".`);
}
}
}
//# sourceMappingURL=platformAccessory.js.map