homebridge-plugin-window-blinds-pattern
Version:
Plugin for Homebridge to control window blinds with a regex pattern.
232 lines • 10.7 kB
JavaScript
import fetch from 'node-fetch';
// Default constants for window blinds
const DEF_MIN_OPEN = 0;
const DEF_MAX_OPEN = 100;
const DEF_TIMEOUT = 5000;
/**
* Extract value from a string using a regex pattern
*/
function extractValueFromPattern(pattern, string, position = 1) {
const matchArray = string.match(pattern);
if (matchArray === null) { // pattern didn't match at all
throw new Error(`Pattern didn't match (value: '${string}', pattern: '${pattern}')`);
}
else if (position >= matchArray.length) {
throw new Error('Couldn\'t find any group which can be extracted. The specified group from which the data should be extracted was out of bounds');
}
else {
return matchArray[position];
}
}
/**
* 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 WindowBlindsPatternPlatformAccessory {
platform;
accessory;
service;
// Configuration properties
name;
debug;
model;
manufacturer;
outputValueMultiplier;
urlSetTargetPosition;
urlGetCurrentPosition;
statusPattern;
matchingGroup;
serial;
timeout;
minOpen;
maxOpen;
// Window blinds state variables
currentPosition = 0;
targetPosition = 100;
positionState;
constructor(platform, accessory) {
this.platform = platform;
this.accessory = accessory;
// Get device configuration from context
const device = accessory.context.device;
// Initialize configuration properties from device context
this.name = device.displayName ?? 'Window Blinds';
this.debug = device.debug ?? false;
this.model = device.model ?? 'nodeMCU based DIY motorised blinds';
this.manufacturer = device.manufacturer ?? '@carlosfrutos';
this.outputValueMultiplier = device.outputValueMultiplier ?? 1;
this.urlSetTargetPosition = device.urlSetTargetPosition;
this.urlGetCurrentPosition = device.urlGetCurrentPosition;
// Initialize status pattern from configuration, default to "(\d+)"
this.statusPattern = /(\d+)/;
if (device.statusPattern) {
if (typeof device.statusPattern === 'string') {
try {
this.statusPattern = new RegExp(device.statusPattern);
}
catch (error) {
this.platform.log.warn(`Invalid regex pattern provided. Using default pattern. ${error instanceof Error ? error.message : String(error)}`);
}
}
else {
this.platform.log.warn('Property \'statusPattern\' was given in an unsupported type. Using default one!');
}
}
// Initialize matching group from configuration
this.matchingGroup = 1;
if (device.matchingGroup) {
if (typeof device.matchingGroup === 'number' && Number.isInteger(device.matchingGroup)) {
this.matchingGroup = device.matchingGroup;
}
else {
this.platform.log.warn('Property \'matchingGroup\' was given in an unsupported type. Using default one!');
}
}
this.serial = device.serial ?? 'HWB02';
this.timeout = device.timeout ?? DEF_TIMEOUT;
this.minOpen = device.minOpen ?? DEF_MIN_OPEN;
this.maxOpen = device.maxOpen ?? DEF_MAX_OPEN;
this.positionState = this.platform.Characteristic.PositionState.STOPPED;
// Set accessory information
this.accessory.getService(this.platform.Service.AccessoryInformation)
.setCharacteristic(this.platform.Characteristic.Manufacturer, this.manufacturer)
.setCharacteristic(this.platform.Characteristic.Model, this.model)
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.serial);
// Get the WindowCovering service if it exists, otherwise create a new WindowCovering service
this.service = this.accessory.getService(this.platform.Service.WindowCovering) ??
this.accessory.addService(this.platform.Service.WindowCovering);
// Set the service name, this is what is displayed as the default name on the Home app
this.service.setCharacteristic(this.platform.Characteristic.Name, this.name);
// Register handlers for the required characteristics
this.service.getCharacteristic(this.platform.Characteristic.Name)
.onGet(this.getName.bind(this));
this.service.getCharacteristic(this.platform.Characteristic.CurrentPosition)
.onGet(this.getCurrentPosition.bind(this));
this.service.getCharacteristic(this.platform.Characteristic.TargetPosition)
.onGet(this.getTargetPosition.bind(this))
.onSet(this.setTargetPosition.bind(this));
this.service.getCharacteristic(this.platform.Characteristic.PositionState)
.onGet(this.getPositionState.bind(this));
// Set initial position state
this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.platform.Characteristic.PositionState.STOPPED);
}
/**
* Handle requests to get the accessory name
*/
async getName() {
this.platform.log.debug('getName:', this.name);
return this.name;
}
/**
* Handle requests to get the current value of the "Current Position" characteristic
*/
async getCurrentPosition() {
if (this.debug) {
this.platform.log.debug('GET CurrentPosition');
}
// Return immediately with cached value if no URL is configured
if (!this.urlGetCurrentPosition) {
return this.currentPosition;
}
try {
// Setup AbortController for timeout functionality
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(this.urlGetCurrentPosition, {
method: 'GET',
signal: controller.signal,
});
// Clear timeout as request completed
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const body = await response.text();
try {
const extractedValue = extractValueFromPattern(this.statusPattern, body, this.matchingGroup);
const value = parseInt(extractedValue, 10);
if (this.debug) {
this.platform.log.debug(`Matched value: ${extractedValue}. Window blind's current position is ${value}`);
}
if (value < this.minOpen || value > this.maxOpen || isNaN(value)) {
throw new Error(`Invalid value received: ${value}`);
}
this.currentPosition = value;
this.service.updateCharacteristic(this.platform.Characteristic.CurrentPosition, this.currentPosition);
this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.platform.Characteristic.PositionState.STOPPED);
return this.currentPosition;
}
catch (parseErr) {
if (parseErr instanceof Error) {
this.platform.log.error(`Error processing received information: ${parseErr.message} body: ${body}`);
}
else {
this.platform.log.error(`Error processing received information: ${String(parseErr)} body: ${body}`);
}
throw parseErr;
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.platform.log.error(`HTTP bad response (${this.urlGetCurrentPosition}): ${errorMessage}`);
throw error;
}
}
/**
* Handle requests to get the current value of the "Target Position" characteristic
*/
async getTargetPosition() {
// Set position state to stopped
this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.platform.Characteristic.PositionState.STOPPED);
return this.targetPosition;
}
/**
* Handle requests to set the "Target Position" characteristic
*/
async setTargetPosition(value) {
const numValue = value;
if (this.debug) {
this.platform.log.debug(`SET TargetPosition from ${this.targetPosition} to ${numValue}`);
}
this.targetPosition = numValue;
if (this.targetPosition > this.currentPosition) {
this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.platform.Characteristic.PositionState.INCREASING);
}
else if (this.targetPosition < this.currentPosition) {
this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.platform.Characteristic.PositionState.DECREASING);
}
else {
this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.platform.Characteristic.PositionState.STOPPED);
}
// Return immediately if no URL is configured
if (!this.urlSetTargetPosition) {
return;
}
const url = this.urlSetTargetPosition.replace('%VALUE%', Math.round(numValue * this.outputValueMultiplier).toString());
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Update current position after movement completes
this.currentPosition = this.targetPosition;
this.service.updateCharacteristic(this.platform.Characteristic.CurrentPosition, this.currentPosition);
this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.platform.Characteristic.PositionState.STOPPED);
if (this.debug) {
this.platform.log.debug(`currentPosition is now ${this.currentPosition}`);
}
}
catch (error) {
this.platform.log.error(`HTTP error when setting position: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Handle requests to get the current value of the "Position State" characteristic
*/
async getPositionState() {
return this.positionState;
}
}
//# sourceMappingURL=platformAccessory.js.map