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, Air Quality, Smoke, Carbon Dioxide and Light
357 lines • 17.3 kB
JavaScript
import axios from 'axios';
import mqtt from 'mqtt';
import { SharedPolling } from './lib/SharedPolling.js'; // Include shared polling library
import { discordWebHooks } from './lib/discordWebHooks.js';
import { getNestedValue } from './lib/utilities.js';
import { sensorConfig } from './platformSensorGenericSettings.js';
/**
* 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 platformSensorGeneric {
platform;
accessory;
sensorService;
mqttClient;
sharedPollingInstance;
enableLogging = true;
// Ensure backward compatibility for shared polling
sharedPolling = false; // Default to false
sharedPollingId = ''; // Default to empty
sharedPollingInterval = 60000;
deviceId = '';
deviceType = '';
deviceName = '';
deviceManufacturer = '';
deviceModel = '';
deviceSerialNumber = '';
deviceFirmwareVersion = '';
urlStatus = '';
mqttReconnectInterval = '';
mqttBroker = '';
mqttPort = '';
mqttUsername = '';
mqttPassword = '';
updateInterval = 60000;
discordWebhook = '';
discordUsername = '';
discordAvatar = '';
discordMessage = '';
paramNames = {};
mqttTopics = {};
SensorStates = {};
SensorStatusRanges = {};
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;
this.urlStatus = device.urlStatus;
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.mqttUsername = device.mqttUsername;
this.mqttPassword = device.mqttPassword;
// Check if deviceType is set and if configuration exists for the current deviceType
if (!this.deviceType) {
this.platform.log.warn('Device type is NOT SET.');
return;
}
const config = sensorConfig[this.deviceType];
if (!config) {
this.platform.log.warn(`Device type ${this.deviceType} is not supported.`);
return;
}
// --------------------------------------------------------------------------------
// Initialize paramNames and mqttTopics dynamically
config.paramNames.forEach((paramNameKey) => {
// Explicitly cast paramNameKey to a valid key of config.states
const paramKey = `paramName${config.states[paramNameKey]?.param}`; // Add "paramName" prefix
if (!paramKey) {
this.platform.log.warn(`Param not found for ${paramNameKey} in states.`);
return;
}
const paramValue = device[paramKey]?.toString() || '';
this.platform.log.debug(`Param Key: ${paramKey}, Value: ${paramValue}`);
this.paramNames[paramNameKey] = paramValue;
// Populate mqttTopics dynamically (using "mqtt" prefix for keys)
const deviceMqttKey = `mqtt${config.states[paramNameKey]?.topic}`; // Add "paramName" prefix
const mqttValue = device[deviceMqttKey]?.toString() || '';
this.platform.log.debug(`MQTT Key: ${deviceMqttKey}, Value: ${mqttValue}`);
this.mqttTopics[paramNameKey] = mqttValue;
});
// Initialize SensorStates and SensorStatusRanges dynamically
Object.entries(config.sensors).forEach(([sensorKey, sensorConfig]) => {
this.SensorStates[sensorKey] = sensorConfig.defaultValue;
this.SensorStatusRanges[sensorKey] = sensorConfig.range;
});
// ---------------------------------------------------------------------------------
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;
// 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.urlStatus, this.platform, this.sharedPollingInterval);
// Subscribe to data updates
sharedPollingInstance.on('dataUpdated', (data) => {
this.updateSensorStatusFromSharedData(data);
});
}
else if (this.urlStatus) {
this.getSensorState();
setInterval(this.getSensorState.bind(this), this.updateInterval);
}
if (this.urlStatus || 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);
// Define a mapping between device types and their corresponding services
const serviceMappings = {
OccupancySensor: this.platform.Service.OccupancySensor,
SmokeSensor: this.platform.Service.SmokeSensor,
CarbonDioxideSensor: this.platform.Service.CarbonDioxideSensor,
AirQualitySensor: this.platform.Service.AirQualitySensor,
MotionSensor: this.platform.Service.MotionSensor,
ContactSensor: this.platform.Service.ContactSensor,
LightSensor: this.platform.Service.LightSensor,
// Add more sensors as needed
};
const serviceConstructor = serviceMappings[this.deviceType];
if (!serviceConstructor) {
throw new Error(`Unsupported device type: ${this.deviceType}`);
}
// Dynamically create or retrieve the service instance
this.sensorService = this.accessory.getService(serviceConstructor)
|| this.accessory.addService(new serviceConstructor(this.deviceName, this.deviceSerialNumber));
// Set the service name, this is what is displayed as the default name on the Home app
this.sensorService.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.deviceName);
if (this.urlStatus) {
this.getStateDefinition().forEach(({ state, param }) => {
if (param) {
const characteristic = this.platform.Characteristic[state];
this.sensorService
.getCharacteristic(characteristic)
.on('get', (callback) => {
callback(null, this.SensorStates[state]); // Use correct state reference
});
}
});
}
// We can now use MQTT
if (this.mqttBroker) {
this.initMQTT();
}
}
}
// Silly function :)
getStatus(isOn) {
return isOn ? 'ON' : 'OFF';
}
getStateDefinition() {
const config = sensorConfig[this.deviceType]?.states;
if (!config) {
this.platform.log.warn(`${this.deviceName}: No configuration found for device type: ${this.deviceType}`);
}
return Object.entries(config).map(([state, stateConfig]) => ({
state,
param: this.paramNames[state],
topic: this.mqttTopics[state],
webhook: stateConfig.webhook,
}));
}
processSensorState(data, isSharedData) {
if (!data) {
this.platform.log.warn(`${this.deviceName}: No data available for ${isSharedData ? 'shared data update' : 'fetching JSON state'}.`);
return;
}
this.getStateDefinition().forEach(({ state, param, webhook }) => {
if (!param) {
if (this.enableLogging) {
this.platform.log.debug(`${this.deviceName}: Parameter for ${state} is not configured. Skipping.`);
}
return;
}
const rawValue = getNestedValue(data, param, 'number');
let value;
if (typeof rawValue === 'number') {
value = rawValue;
}
else if (typeof rawValue === 'boolean') {
value = rawValue ? 1 : 0; // Convert boolean to number
}
else {
value = undefined; // Treat invalid types as undefined
}
if (value === undefined) {
if (this.enableLogging) {
this.platform.log.warn(`${this.deviceName}: Parameter '${param}' not found in JSON for state ${state}.`);
}
return;
}
const range = this.SensorStatusRanges[state];
if (Array.isArray(range) && range.length === 2 &&
typeof range[0] === 'number' && typeof range[1] === 'number' &&
value >= range[0] && value <= range[1]) {
if (this.enableLogging && this.SensorStates[state] !== value) {
this.platform.log.info(`${this.deviceName}: ${state} SET to: ${value}`);
}
this.SensorStates[state] = value;
const characteristic = this.platform.Characteristic[state];
this.sensorService.updateCharacteristic(characteristic, value);
if (webhook && value === 1) {
this.initDiscordWebhooks(state);
}
}
else if (this.enableLogging) {
this.platform.log.warn(`${this.deviceName}: Received invalid ${state} value: ${value} (valid range: ${range[0]} to ${range[1]}).`);
}
});
}
updateSensorStatusFromSharedData(data) {
this.processSensorState(data, true);
}
async getSensorState() {
if (!this.urlStatus) {
this.platform.log.warn(`${this.deviceName}: Ignoring request; No status URL defined.`);
return;
}
try {
const response = await axios.get(this.urlStatus, { timeout: 8000 });
const data = response.data;
this.processSensorState(data, false);
}
catch (error) {
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.`);
}
}
}
initMQTT() {
// Define an empty array to hold the subscribed topics
const mqttSubscribedTopics = [];
// Prepare MQTT connection options
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,
};
// Use getStateDefinition to dynamically fetch topics and push them into the array
this.getStateDefinition().forEach(({ topic }) => {
if (typeof topic === 'string' && topic.trim().length > 0) {
mqttSubscribedTopics.push(topic); // Push valid topic
}
});
// Initialize MQTT client
this.mqttClient = mqtt.connect(mqttOptions);
this.mqttClient.on('connect', () => {
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, ': Subscribing problem: ', err.toString());
}
});
});
// Handle incoming MQTT messages
this.mqttClient.on('message', (topic, message) => {
this.getStateDefinition().forEach(({ state, topic: stateTopic, webhook }) => {
if (stateTopic === topic) { // Match incoming topic
const value = message.toString();
let newValue;
// Handle binary and numeric ranges dynamically
const [min, max] = this.SensorStatusRanges[state];
if (min === 0 && max === 1) {
newValue = ['1', 'true'].includes(value) ? 1 : 0; // Binary range
}
else {
newValue = Number(value); // Numeric range
}
// Validate against SensorStatusRanges
if (newValue >= min && newValue <= max) {
this.SensorStates[state] = newValue; // Update state value
if (this.enableLogging) {
this.platform.log.info(`${this.deviceName}: ${state} set to: ${newValue}`);
}
// Update Homebridge characteristic
const characteristic = this.platform.Characteristic[state];
this.sensorService.updateCharacteristic(characteristic, newValue);
// Trigger webhook if `webhook` is true and `newValue === 1`
if (webhook && newValue === 1) {
this.initDiscordWebhooks(state);
}
}
else {
if (this.enableLogging) {
this.platform.log.warn(`${this.deviceName}: Invalid value for ${state}: ${newValue} (must be between ${min} and ${max})`);
}
}
}
});
});
// Additional event handlers for connection state
this.mqttClient.on('offline', () => {
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.platform.log.debug(this.deviceName, ': Connection closed');
});
// Enhanced error handling
this.mqttClient.on('error', (err) => {
this.platform.log.warn(this.deviceName, ': Connection error:', err);
this.platform.log.warn(this.deviceName, ': Reconnecting in: ', this.mqttReconnectInterval, ' seconds.');
});
}
initDiscordWebhooks(state) {
// Prepare a dynamic message including the passed state
const message = `${this.deviceName}: ${state} - ${this.discordMessage} ${this.getStatus(!!this.SensorStates[state])}`;
const discord = new discordWebHooks(this.discordWebhook, this.discordUsername, this.discordAvatar, message);
discord.discordSimpleSend().then((result) => {
if (this.enableLogging) {
this.platform.log.info(`${this.deviceName}: Webhook sent successfully - `, result);
}
}).catch((error) => {
if (this.enableLogging) {
this.platform.log.warn(`${this.deviceName}: Failed to send webhook - `, error.message);
}
});
}
}
//# sourceMappingURL=platformSensorGenericServices.js.map