homebridge-http-sensors-switches
Version:
This plugin communicates with your devices over HTTP or MQTT. Currently it supports Light Bulb, Switches, Outlets, Fan, Garage Door, Shades / Blinds, Temperature/Humidity, Motion, Contact and Occupancy sensor, Door, Sprinkler, Valve, Air Quality, Smoke, C
421 lines • 19.6 kB
JavaScript
import { SharedPolling } from './lib/SharedPolling.js'; // Include shared polling library
import { getNestedValue, hasNestedKey } from './lib/utilities.js'; // Include utility function for nested value retrieval
import { discordWebHooks } from './lib/discordWebHooks.js'; // Include Discord webhook library
import { HttpsAgentManager } from './lib/HttpsAgentManager.js';
import axios from 'axios';
import mqtt from 'mqtt';
export class platformOutlet {
platform;
accessory;
service;
mqttClient;
sharedPollingInstance;
isReachable = true; // Track if the device is reachable
// Device and configuration properties
enableLogging = true;
// Security, Self Signed Certificates rules
ignoreHttpsCertErrors = false;
trustedCert;
// Ensure backward compatibility for shared polling
sharedPolling = false; // Default to false
sharedPollingId = ''; // Default to empty
sharedPollingInterval = 5000;
deviceId = '';
deviceType = '';
deviceName = '';
deviceManufacturer = '';
deviceModel = '';
deviceSerialNumber = '';
deviceFirmwareVersion = '';
urlON = '';
urlOFF = '';
url = '';
urlStatus = '';
statusStateParam = '';
statusOnCheck = '';
statusOffCheck = '';
inUseStateParam = '';
inUseOnCheck = '';
inUseOffCheck = '';
mqttReconnectInterval = '';
mqttBroker = '';
mqttPort = '';
mqttSwitch = '';
mqttInUse = '';
mqttUsername = '';
mqttPassword = '';
discordWebhook = '';
discordUsername = '';
discordAvatar = '';
discordMessage = '';
outletStates = {
On: false,
OutletInUse: false,
};
httpsAgentManager;
constructor(platform, accessory) {
this.platform = platform;
this.accessory = accessory;
const device = this.accessory.context.device;
this.deviceType = device.deviceType;
this.deviceName = device.deviceName || 'NoName';
this.deviceManufacturer = device.deviceManufacturer || 'Stergo';
this.deviceModel = device.deviceModel || 'Outlet';
this.deviceSerialNumber = device.deviceSerialNumber || accessory.UUID;
this.deviceFirmwareVersion = device.deviceFirmwareVersion || '0.0';
// From config
this.enableLogging = device.enableLogging;
// Security, Self Signed Certificates rules
this.ignoreHttpsCertErrors = device.ignoreHttpsCertErrors || false;
this.trustedCert = device.trustedCert || undefined;
this.urlStatus = device.urlStatus;
this.statusStateParam = device.stateName;
this.statusOnCheck = device.onStatusValue;
this.statusOffCheck = device.offStatusValue;
this.urlON = device.urlON;
this.urlOFF = device.urlOFF;
this.inUseStateParam = device.inUseStateName;
this.inUseOnCheck = device.inUseOnStatusValue;
this.inUseOffCheck = device.inUseOffStatusValue;
this.mqttReconnectInterval = device.mqttReconnectInterval || 60; // 60 sec default
this.mqttBroker = device.mqttBroker;
this.mqttPort = device.mqttPort;
this.mqttSwitch = device.mqttSwitch;
this.mqttInUse = device.mqttInUse;
this.mqttUsername = device.mqttUsername;
this.mqttPassword = device.mqttPassword;
this.discordWebhook = device.discordWebhook;
this.discordUsername = device.discordUsername || 'StergoSmart';
this.discordAvatar = device.discordAvatar || 'https://raw.githubusercontent.com/homebridge/branding/latest/logos/homebridge-color-round-stylized.png';
this.discordMessage = device.discordMessage;
this.httpsAgentManager = new HttpsAgentManager(this.trustedCert, this.ignoreHttpsCertErrors, this.urlStatus);
// Ensure backward compatibility for shared polling
this.sharedPolling = device.sharedPolling ?? false; // Default shared polling to false
this.sharedPollingId = device.sharedPollingId ?? ''; // Default shared polling group ID to an empty string
this.sharedPollingInterval = device.sharedPollingInterval ?? 5000; // Set the polling interval to 5 sec or from config value
if (this.sharedPolling && this.sharedPollingId) {
const sharedPollingInstance = SharedPolling.registerPolling(this.sharedPollingId, this.urlStatus, this.platform, this.sharedPollingInterval, // Set the polling interval to 60 sec or from config value
this.httpsAgentManager);
// Subscribe to data updates
sharedPollingInstance.on('dataUpdated', (data) => {
this.isReachable = true; // ✅ Mark as reachable
this.updateOutletStatusFromSharedData(data);
});
sharedPollingInstance.on('dataError', () => {
this.isReachable = false; // ❌ Mark as unreachable
});
}
else if (this.urlStatus) {
this.getOn();
setInterval(this.getOn.bind(this), 5000);
}
if (!this.deviceType) {
this.platform.log.warn(this.deviceName, ': Ignoring accessory; No deviceType defined.');
return;
}
if (this.deviceType === 'Outlet' && (this.urlON || this.mqttBroker)) {
this.accessory.getService(this.platform.Service.AccessoryInformation)
.setCharacteristic(this.platform.Characteristic.Manufacturer, this.deviceManufacturer)
.setCharacteristic(this.platform.Characteristic.Model, this.deviceModel)
.setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.deviceFirmwareVersion)
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.deviceSerialNumber);
if (this.urlON || this.mqttBroker) {
this.service = this.accessory.getService(this.platform.Service.Outlet) || this.accessory.addService(this.platform.Service.Outlet);
this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.deviceName);
if (this.urlON) {
this.service.getCharacteristic(this.platform.Characteristic.On)
.on('set', this.wrapSetHandler()) // Reuses setOn
.on('get', this.wrapGetHandler('On'));
}
if (this.inUseOnCheck) {
this.service.getCharacteristic(this.platform.Characteristic.OutletInUse)
.on('get', this.wrapGetHandler('OutletInUse'));
}
if (this.mqttBroker) {
this.initMQTT();
this.service.getCharacteristic(this.platform.Characteristic.On)
.on('set', this.publishMQTTmessage.bind(this));
}
}
}
}
wrapGetHandler(state) {
return (callback) => {
if (!this.isReachable) {
callback(new this.platform.api.hap.HapStatusError(-70402 /* this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE */));
return;
}
callback(null, this.outletStates[state]);
};
}
wrapSetHandler() {
return (value, callback) => {
if (!this.isReachable) {
callback(new this.platform.api.hap.HapStatusError(-70402 /* this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE */));
return;
}
this.setOn(value, callback); // Preserves your original logic
};
}
getStatus(isOn) {
return isOn ? 'ON' : 'OFF';
}
updateOutletState(isOn, deviceName) {
if (this.outletStates.On !== isOn) {
this.outletStates.On = isOn;
if (this.enableLogging) {
this.platform.log.info(deviceName, `: Outlet is ${isOn ? 'ON' : 'OFF'}`);
}
this.service.updateCharacteristic(this.platform.Characteristic.On, isOn);
}
}
updateOutletStatusFromSharedData(data) {
this.processOutletGetData(data, true);
}
async getOn() {
if (!this.urlStatus) {
this.platform.log.warn(this.deviceName, ': Ignoring request; No status url defined.');
return;
}
try {
this.isReachable = true; // ✅ Mark as reachable
const httpsAgent = this.httpsAgentManager?.getAgent(); // Use HTTPS agent if applicable
const response = await axios.get(this.urlStatus, { timeout: 8000, httpsAgent });
const data = response.data;
this.platform.log.debug(`${this.deviceName}: Fetched JSON data:`, data);
this.processOutletGetData(data, false);
}
catch (error) {
this.isReachable = false; // ❌ Mark as unreachable
const axiosError = error;
if (axios.isAxiosError(axiosError)) {
this.platform.log.warn(`${this.deviceName}: Axios error while fetching JSON:`, axiosError.message);
}
else {
this.platform.log.warn(`${this.deviceName}: Unknown error occurred while fetching JSON.`);
}
}
}
processOutletGetData(data, isSharedData) {
if (!data) {
this.platform.log.warn(`${this.deviceName}: No data available for ${isSharedData ? 'shared data update' : 'fetching Outlet state'}.`);
return;
}
if (this.statusStateParam && hasNestedKey(data, this.statusStateParam)) {
const value = getNestedValue(data, this.statusStateParam, 'string'); // Adjust returnType as needed
const valueType = typeof value;
let statusOnCheck;
let statusOffCheck;
if (valueType === 'boolean') {
statusOnCheck = true;
statusOffCheck = false;
}
else if (valueType === 'number') {
statusOnCheck = parseFloat(this.statusOnCheck);
statusOffCheck = parseFloat(this.statusOffCheck);
}
else {
statusOnCheck = this.statusOnCheck;
statusOffCheck = this.statusOffCheck;
}
if (value === statusOnCheck) {
this.updateOutletState(true, this.deviceName);
}
else if (value === statusOffCheck) {
this.updateOutletState(false, this.deviceName);
}
else {
this.platform.log.warn(this.deviceName, `: The value of ${this.statusStateParam} does not match statusOnCheck or statusOffCheck.`);
}
}
if (this.inUseStateParam && hasNestedKey(data, this.inUseStateParam)) {
const value = getNestedValue(data, this.inUseStateParam, 'string'); // Adjust returnType as needed
const valueType = typeof value;
let inUseOnCheck;
let inUseOffCheck;
if (valueType === 'boolean') {
inUseOnCheck = true;
inUseOffCheck = false;
}
else if (valueType === 'number') {
inUseOnCheck = parseFloat(this.inUseOnCheck);
inUseOffCheck = parseFloat(this.inUseOffCheck);
}
else {
inUseOnCheck = this.inUseOnCheck;
inUseOffCheck = this.inUseOffCheck;
}
// Update OutletInUse characteristic
if (value === inUseOnCheck) {
if (this.enableLogging && this.outletStates.OutletInUse !== true) {
this.platform.log.info(this.deviceName, ': inUse set to: ', this.getStatus(true));
}
this.outletStates.OutletInUse = true;
}
else if (value === inUseOffCheck) {
if (this.enableLogging && this.outletStates.OutletInUse !== false) {
this.platform.log.info(this.deviceName, ': inUse set to: ', this.getStatus(false));
}
this.outletStates.OutletInUse = false;
}
else {
this.platform.log.warn(this.deviceName, `: The value of ${this.inUseStateParam} does not match inUseOnCheck or inUseOffCheck.`);
}
this.service.updateCharacteristic(this.platform.Characteristic.OutletInUse, this.outletStates.OutletInUse);
}
else {
if (this.inUseStateParam) {
this.platform.log.warn(this.deviceName, ': Error: Cannot find KEY:', this.statusStateParam, 'in JSON');
}
}
}
async setOn(value, callback) {
this.outletStates.On = value;
if (!this.urlON || !this.urlOFF) {
this.platform.log.warn(this.deviceName, ': Ignoring request; No Outlet trigger url defined.');
callback(new Error('No Outlet trigger url defined.'));
return;
}
if (this.outletStates.On) {
this.url = this.urlON;
this.platform.log.debug(this.deviceName, ': Setting power state to ON');
this.service.updateCharacteristic(this.platform.Characteristic.On, true);
}
else {
this.url = this.urlOFF;
this.platform.log.debug(this.deviceName, ': Setting power state to OFF');
this.service.updateCharacteristic(this.platform.Characteristic.On, false);
}
axios.get(this.url)
.then(() => {
this.isReachable = true; // ✅ Mark as reachable
this.outletStates.OutletInUse = this.outletStates.On;
this.service.updateCharacteristic(this.platform.Characteristic.OutletInUse, this.outletStates.OutletInUse);
if (this.enableLogging) {
this.platform.log.info('Success: Outlet ', this.deviceName, ' is: ', this.getStatus(this.outletStates.On));
}
})
.catch((error) => {
this.isReachable = false; // ❌ Mark as unreachable
this.outletStates.On = !value;
this.service.updateCharacteristic(this.platform.Characteristic.On, this.outletStates.On);
this.platform.log.warn(this.deviceName, ': Setting power state to :', this.outletStates.On);
this.platform.log.warn(this.deviceName, ': Error: ', error.message);
});
if (this.discordWebhook) {
this.initDiscordWebhooks();
}
callback(null);
}
//
// Connect to MQTT and update Outlets
initMQTT() {
const mqttSubscribedTopics = [];
const mqttOptions = {
keepalive: 10,
protocol: 'mqtt',
host: this.mqttBroker,
port: Number(this.mqttPort),
clientId: this.deviceName,
clean: true,
username: this.mqttUsername,
password: this.mqttPassword,
rejectUnauthorized: false,
reconnectPeriod: Number(this.mqttReconnectInterval) * 1000,
};
if (this.mqttSwitch) {
mqttSubscribedTopics.push(this.mqttSwitch);
}
if (this.mqttInUse) {
mqttSubscribedTopics.push(this.mqttInUse);
}
this.mqttClient = mqtt.connect(mqttOptions);
this.mqttClient.on('connect', () => {
this.isReachable = true; // ✅ Mark as reachable
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': MQTT Connected');
}
this.mqttClient.subscribe(mqttSubscribedTopics, (err) => {
if (!err) {
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Subscribed to: ', mqttSubscribedTopics.toString());
}
}
else {
// Need to insert error handler
this.platform.log.warn(this.deviceName, err.toString());
}
});
});
this.mqttClient.on('message', (topic, message) => {
if (topic === this.mqttSwitch) {
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Status set to: ', this.getStatus(Boolean(Number(message))));
}
if (message.toString() === '1' || message.toString() === 'true') {
this.outletStates.On = true;
}
if (message.toString() === '0' || message.toString() === 'false') {
this.outletStates.On = false;
}
this.service.updateCharacteristic(this.platform.Characteristic.On, this.outletStates.On);
// If discordWebhook is set
if (this.discordWebhook) {
this.initDiscordWebhooks();
}
}
if (topic === this.mqttInUse) {
this.platform.log.info(this.deviceName, ': inUse set to: ', this.getStatus(Boolean(Number(message))));
if (message.toString() === '1' || message.toString() === 'true') {
this.outletStates.OutletInUse = true;
}
if (message.toString() === '0' || message.toString() === 'false') {
this.outletStates.OutletInUse = false;
}
this.service.updateCharacteristic(this.platform.Characteristic.OutletInUse, this.outletStates.OutletInUse);
}
});
this.mqttClient.on('offline', () => {
this.isReachable = false; // ❌ Mark as unreachable
this.platform.log.debug(this.deviceName, ': Client is offline');
});
this.mqttClient.on('reconnect', () => {
this.platform.log.debug(this.deviceName, ': Reconnecting...');
});
this.mqttClient.on('close', () => {
this.isReachable = false; // ❌ Mark as unreachable
this.platform.log.debug(this.deviceName, ': Connection closed');
});
// Handle errors
this.mqttClient.on('error', (err) => {
this.isReachable = false; // ❌ Mark as unreachable
this.platform.log.warn(this.deviceName, ': Connection error:', err);
this.platform.log.warn(this.deviceName, ': Reconnecting in: ', this.mqttReconnectInterval, ' seconds.');
});
}
// Function to publish a message
publishMQTTmessage(value, callback) {
this.platform.log.debug(this.deviceName, ': Setting power state to:', this.getStatus(!this.outletStates.On));
this.mqttClient.publish(this.mqttSwitch, String(Number(!this.outletStates.On)), { qos: 1, retain: true }, (err) => {
if (err) {
this.platform.log.debug(this.deviceName, ': Failed to publish message: ', err);
}
else {
this.service.updateCharacteristic(this.platform.Characteristic.On, this.outletStates.On);
this.platform.log.debug(this.deviceName, ': Message published successfully');
}
});
callback(null);
}
initDiscordWebhooks() {
// Prepare message just to send On Off status
const message = this.deviceName + ': ' + this.discordMessage + this.getStatus(this.outletStates.On);
const discord = new discordWebHooks(this.discordWebhook, this.discordUsername, this.discordAvatar, message);
discord.discordSimpleSend().then((result) => {
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': ', result);
}
});
}
}
//# sourceMappingURL=platformOutletServices.js.map