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
451 lines • 23.6 kB
JavaScript
import { HttpsAgentManager } from './lib/HttpsAgentManager.js';
import axios from 'axios';
import mqtt from 'mqtt';
import { SharedPolling } from './lib/SharedPolling.js'; // Include shared polling library
import { getNestedValue } from './lib/utilities.js';
import { discordWebHooks } from './lib/discordWebHooks.js'; // Include Discord webhook library
/**
* Platform Accessory
* An instance of this class is created for each accessory your platform registers
* Each accessory may expose multiple services of different service types.
*/
export class platformSensors {
platform;
accessory;
temperatureService;
humidityService;
batteryService;
mqttClient;
sharedPollingInstance;
isReachable = true; // Track if the device is reachable
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 = 60000; // Default to 60 seconds
deviceId = '';
deviceType = '';
deviceName = '';
deviceManufacturer = '';
deviceModel = '';
deviceSerialNumber = '';
deviceFirmwareVersion = '';
sensorUrl = '';
temperatureName = '';
humidityName = '';
airPressureName = '';
batteryLevelName = '';
batteryChargingStateName = '';
batteryStatusLowName = '';
mqttReconnectInterval = '';
mqttBroker = '';
mqttPort = '';
mqttTemperature = '';
mqttHumidity = '';
mqttBatteryLevel = '';
mqttBatteryChargingState = '';
mqttBatteryStatusLow = '';
mqttUsername = '';
mqttPassword = '';
discordWebhook = '';
discordUsername = '';
discordAvatar = '';
discordMessage = '';
currentTemperature = 20;
currentHumidity = 50;
currentBatteryLevel = 78;
currentBatteryChargingState = 2; // 0: Not charging, 1: Charging, 2: Not chargeable
currentBatteryStatusLow = false;
updateInterval = 300000;
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 || 'Sensor';
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.sensorUrl = device.sensorUrl;
this.temperatureName = device.temperatureName;
this.humidityName = device.humidityName;
this.airPressureName = device.airPressureName;
this.batteryLevelName = device.paramNameBatteryLevel;
this.batteryChargingStateName = device.paramNameStatusChargingBattery;
this.batteryStatusLowName = device.paramNameStatusLowBattery;
this.updateInterval = device.updateInterval || 60000; // Default update interval is 300 seconds
this.mqttReconnectInterval = device.mqttReconnectInterval || 60; // 60 sec default
this.mqttBroker = device.mqttBroker;
this.mqttPort = device.mqttPort;
this.mqttTemperature = device.mqttTemperature;
this.mqttHumidity = device.mqttHumidity;
this.mqttBatteryLevel = device.mqttBatteryLevel;
this.mqttBatteryChargingState = device.mqttBatteryChargingState;
this.mqttBatteryStatusLow = device.mqttLowBattery;
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.sensorUrl);
// 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 ?? 60000; // Set the polling interval to 60 sec or from config value
if (this.sharedPolling && this.sharedPollingId) {
const sharedPollingInstance = SharedPolling.registerPolling(this.sharedPollingId, this.sensorUrl, 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.updateSensorStatusFromSharedData(data);
});
sharedPollingInstance.on('dataError', () => {
this.isReachable = false; // ❌ Mark as unreachable
});
}
else if (this.sensorUrl) {
this.getSensorData();
setInterval(this.getSensorData.bind(this), this.updateInterval);
}
if (!this.deviceType) {
return;
}
if (this.deviceType === 'Sensor' && (this.sensorUrl || this.mqttBroker)) {
// set accessory information
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 we have Config setup for Temperature
if (this.temperatureName || this.mqttTemperature) {
// get the TemperatureSensor service if it exists, otherwise create a new TemperatureSensor service
this.temperatureService = this.accessory.getService(this.platform.Service.TemperatureSensor)
|| this.accessory.addService(this.platform.Service.TemperatureSensor);
// set the service name, this is what is displayed as the default name on the Home app
// in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method.
this.temperatureService.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.deviceName);
//this.service = this.service.addCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity);
// register handlers for the CurrentTemperature Characteristic
this.temperatureService.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
.on('get', this.wrapGetHandler('currentTemperature'));
}
// If we have Config setup for Humidity
if (this.humidityName || this.mqttHumidity) {
// get the HumiditySensor service if it exists, otherwise create a new HumiditySensor service
this.humidityService = this.accessory.getService(this.platform.Service.HumiditySensor)
|| this.accessory.addService(this.platform.Service.HumiditySensor);
this.humidityService.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.deviceName);
// register handlers for the CurrentRelativeHumidity Characteristic
this.humidityService.getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity)
.on('get', this.wrapGetHandler('currentHumidity'));
}
// If we have Config setup for Air Pressure
// Not implementd yet
if (this.airPressureName) {
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': ', this.airPressureName);
}
}
// If we have Config setup for Battery
if (this.batteryLevelName || this.batteryChargingStateName || this.batteryStatusLowName ||
(this.mqttBatteryLevel || this.mqttBatteryChargingState || this.mqttBatteryStatusLow)) {
this.batteryService = this.accessory.getService(this.platform.Service.Battery)
|| this.accessory.addService(this.platform.Service.Battery);
this.batteryService.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.deviceName + ' Battery');
// Battery Level
if (this.batteryLevelName || this.mqttBatteryLevel) {
this.batteryService.getCharacteristic(this.platform.Characteristic.BatteryLevel)
.on('get', this.wrapGetHandler('currentBatteryLevel'));
}
// Charging State
if (this.batteryChargingStateName || this.mqttBatteryChargingState) {
this.batteryService.getCharacteristic(this.platform.Characteristic.ChargingState)
.on('get', this.wrapGetHandler('currentBatteryChargingState'));
}
// Status Low Battery
if (this.batteryStatusLowName || this.mqttBatteryStatusLow) {
this.batteryService.getCharacteristic(this.platform.Characteristic.StatusLowBattery)
.on('get', this.wrapGetHandler('currentBatteryStatusLow'));
}
}
// We can now use MQTT
if (this.mqttBroker) {
this.getSensorDataMQTT();
}
}
}
wrapGetHandler(stateKey) {
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[stateKey]);
};
}
updateSensorStatusFromSharedData(data) {
this.processGetSensorStatus(data, true);
}
async getSensorData() {
if (!this.sensorUrl) {
this.platform.log.warn(`${this.deviceName}: Ignoring request; No status URL defined.`);
return;
}
try {
this.isReachable = true;
const httpsAgent = this.httpsAgentManager.getAgent();
const response = await axios.get(this.sensorUrl, { timeout: 8000, httpsAgent });
const data = response.data;
this.processGetSensorStatus(data, false);
}
catch (error) {
this.isReachable = false;
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.`);
}
}
}
processGetSensorStatus(data, isSharedData) {
if (!data) {
this.platform.log.warn(`${this.deviceName}: No data available for ${isSharedData ? 'shared data update' : 'fetching Switch state'}.`);
return;
}
// If Temperature Service is available
if (this.temperatureService) {
if (this.temperatureName) {
const tmpTemperature = getNestedValue(data, this.temperatureName, 'number');
if (typeof tmpTemperature === 'number') {
this.currentTemperature = tmpTemperature;
this.temperatureService.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.currentTemperature);
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Temperature = ', this.currentTemperature.toString());
}
}
else {
this.platform.log.warn(this.deviceName, ': Error: Cannot find or convert: ', this.temperatureName, ' in JSON');
}
}
else {
this.platform.log.warn(this.deviceName, ': Error: Temperature name is not defined');
}
}
// If Humidity Service is available
if (this.humidityService) {
if (this.humidityName) {
const tmpHumidity = getNestedValue(data, this.humidityName, 'number');
if (typeof tmpHumidity === 'number') {
this.currentHumidity = tmpHumidity;
this.humidityService.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, this.currentHumidity);
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Humidity = ', this.currentHumidity.toString());
}
}
else {
this.platform.log.warn(this.deviceName, ': Error: Cannot find or convert: ', this.humidityName, ' in JSON');
}
}
else {
this.platform.log.warn(this.deviceName, ': Error: Humidity name is not defined');
}
}
// If we have Config setup for Air Pressure
if (this.airPressureName) {
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': ', this.airPressureName);
}
}
// If Battery Service is available
if (this.batteryService) {
if (this.batteryLevelName) {
const tmpBatteryLevel = getNestedValue(data, this.batteryLevelName, 'number');
if (typeof tmpBatteryLevel === 'number') {
this.currentBatteryLevel = tmpBatteryLevel;
this.batteryService.updateCharacteristic(this.platform.Characteristic.BatteryLevel, this.currentBatteryLevel);
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Battery Level = ', this.currentBatteryLevel.toString());
}
}
else {
this.platform.log.warn(this.deviceName, ': Error: Cannot find or convert: ', this.batteryLevelName, ' in JSON');
}
}
if (this.batteryChargingStateName) {
const tmpChargingState = getNestedValue(data, this.batteryChargingStateName, 'number');
if (typeof tmpChargingState === 'number') {
this.currentBatteryChargingState = tmpChargingState;
this.batteryService.updateCharacteristic(this.platform.Characteristic.ChargingState, this.currentBatteryChargingState);
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Battery Charging State = ', this.currentBatteryChargingState.toString());
}
}
else {
this.platform.log.warn(this.deviceName, ': Error: Cannot find or convert: ', this.batteryChargingStateName, ' in JSON');
}
}
if (this.batteryStatusLowName) {
const tmpStatusLow = getNestedValue(data, this.batteryStatusLowName, 'boolean');
if (typeof tmpStatusLow === 'boolean') {
this.currentBatteryStatusLow = tmpStatusLow;
this.batteryService.updateCharacteristic(this.platform.Characteristic.StatusLowBattery, this.currentBatteryStatusLow
? this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
: this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL);
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Battery Status Low = ', this.currentBatteryStatusLow.toString());
}
// ✅ Send Discord webhook only if battery is low
if (this.currentBatteryStatusLow && this.discordWebhook) {
this.initDiscordWebhooks();
}
}
else {
this.platform.log.warn(this.deviceName, ': Error: Cannot find or convert: ', this.batteryStatusLowName, ' in JSON');
}
}
}
if (this.enableLogging) {
// this.platform.log.debug(this.deviceName,': ',JSON.stringify(data));
}
}
//
// Connect to MQTT and update Temperature and Humidity
getSensorDataMQTT() {
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.mqttTemperature) {
mqttSubscribedTopics.push(this.mqttTemperature);
}
if (this.mqttHumidity) {
mqttSubscribedTopics.push(this.mqttHumidity);
}
// Subscribe to battery topics
if (this.mqttBatteryLevel) {
mqttSubscribedTopics.push(this.mqttBatteryLevel);
}
if (this.mqttBatteryChargingState) {
mqttSubscribedTopics.push(this.mqttBatteryChargingState);
}
if (this.mqttBatteryStatusLow) {
mqttSubscribedTopics.push(this.mqttBatteryStatusLow);
}
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) => {
//this.platform.log(this.deviceName,': Received message: ${message.toString()}');
if (topic === this.mqttTemperature) {
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Temperature = ', message.toString());
}
this.currentTemperature = Number(message.toString());
this.temperatureService.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.currentTemperature);
}
if (topic === this.mqttHumidity) {
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Humidity = ', message.toString());
}
this.currentHumidity = Number(message.toString());
this.humidityService.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, this.currentHumidity);
}
if (topic === this.mqttBatteryLevel) {
this.currentBatteryLevel = Number(message.toString());
this.batteryService?.updateCharacteristic(this.platform.Characteristic.BatteryLevel, this.currentBatteryLevel);
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Battery Level = ', this.currentBatteryLevel);
}
}
if (topic === this.mqttBatteryChargingState) {
this.currentBatteryChargingState = Number(message.toString());
this.batteryService?.updateCharacteristic(this.platform.Characteristic.ChargingState, this.currentBatteryChargingState);
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Battery Charging State = ', this.currentBatteryChargingState);
}
}
if (topic === this.mqttBatteryStatusLow) {
const raw = message.toString().trim().toLowerCase();
if (raw === 'true' || raw === 'false') {
this.currentBatteryStatusLow = raw === 'true';
this.batteryService?.updateCharacteristic(this.platform.Characteristic.StatusLowBattery, this.currentBatteryStatusLow
? this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
: this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL);
if (this.enableLogging) {
this.platform.log.info(this.deviceName, ': Battery Status Low = ', raw);
}
}
else {
this.platform.log.warn(this.deviceName, ': Invalid Battery Status Low value:', raw);
}
}
});
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.');
//this.mqttClient.end();
});
}
initDiscordWebhooks() {
const message = `${this.deviceName}: LOW BATTERY WARNING!`;
const discord = new discordWebHooks(this.discordWebhook, this.discordUsername, this.discordAvatar, message);
discord.discordSimpleSend().then((result) => {
this.platform.log.info(`${this.deviceName}: Discord Webhook result - ${result}`);
}).catch((error) => {
this.platform.log.warn(`${this.deviceName}: Discord Webhook error - ${error.message}`);
});
}
}
//# sourceMappingURL=platformSensorServices.js.map