homebridge-soma-shades
Version:
A homebridge plugin for SOMA Smart Shades
223 lines • 12.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ShadesAccessory = void 0;
// battery level below 10% is considered low battery
const LOW_BATTERY_LEVEL = 10;
// refresh every 10 seconds
// TODO: set through configuration
const REFRESH_RATE = 10;
// dummy replica of HAP.PositionState
var POSITION_STATE;
(function (POSITION_STATE) {
POSITION_STATE[POSITION_STATE["DECREASING"] = 0] = "DECREASING";
POSITION_STATE[POSITION_STATE["INCREASING"] = 1] = "INCREASING";
POSITION_STATE[POSITION_STATE["STOPPED"] = 2] = "STOPPED";
})(POSITION_STATE || (POSITION_STATE = {}));
// dummy replica of HAP.ChargingState
var CHARGING_STATE;
(function (CHARGING_STATE) {
CHARGING_STATE[CHARGING_STATE["NOT_CHARGING"] = 0] = "NOT_CHARGING";
CHARGING_STATE[CHARGING_STATE["CHARGING"] = 1] = "CHARGING";
CHARGING_STATE[CHARGING_STATE["NOT_CHARGEABLE"] = 2] = "NOT_CHARGEABLE";
})(CHARGING_STATE || (CHARGING_STATE = {}));
// dummy replica of HAP.LowBattery
var LOW_BATTERY;
(function (LOW_BATTERY) {
LOW_BATTERY[LOW_BATTERY["NORMAL"] = 0] = "NORMAL";
LOW_BATTERY[LOW_BATTERY["LOW"] = 1] = "LOW";
})(LOW_BATTERY || (LOW_BATTERY = {}));
class ShadesAccessory {
constructor(platform, accessory, somaDevice) {
this.platform = platform;
this.accessory = accessory;
this.somaDevice = somaDevice;
/**
* The HomeKit uses position as percentage from bottom to top. 0 is bottom(fully closed) and 100 is top(fully open)
* whereas the shades uses the percentage from top to bottom where 100 is bottom(fully closed) and 0 is top(fully open)
* Here we regulate them to use "HomeKit Standard"
*/
this.shadesState = {
positionState: POSITION_STATE.STOPPED,
currentPosition: 0,
targetPosition: 0,
};
this.batteryState = {
level: 100,
charging: CHARGING_STATE.NOT_CHARGEABLE,
low_battery: LOW_BATTERY.NORMAL,
};
// last time we set target position
this.lastMovementDone = true;
// we want to setup current & target position from device
// but only at the first poll
this.firstPoll = true;
// set up window covering service
this.service = this.accessory.getService(this.platform.Service.WindowCovering) || this.accessory.addService(this.platform.Service.WindowCovering);
this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name);
this.service.getCharacteristic(this.platform.Characteristic.PositionState).updateValue(this.shadesState.positionState);
this.service.getCharacteristic(this.platform.Characteristic.CurrentPosition).updateValue(this.shadesState.currentPosition);
this.service.getCharacteristic(this.platform.Characteristic.TargetPosition).updateValue(this.shadesState.targetPosition);
this.service
.getCharacteristic(this.platform.Characteristic.CurrentPosition)
.on("get" /* GET */, this.getCurrentPosition.bind(this));
this.service
.getCharacteristic(this.platform.Characteristic.PositionState)
.on("get" /* GET */, this.getPositionState.bind(this));
this.service
.getCharacteristic(this.platform.Characteristic.TargetPosition)
.on("get" /* GET */, this.getTargetPosition.bind(this))
.on("set" /* SET */, this.setTargetPosition.bind(this));
// setup battery service
this.batteryService = this.accessory.getService(this.platform.Service.BatteryService) || this.accessory.addService(this.platform.Service.BatteryService);
this.batteryService.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name + ' Battery');
this.batteryService.getCharacteristic(this.platform.Characteristic.BatteryLevel).updateValue(this.batteryState.level);
this.batteryService.getCharacteristic(this.platform.Characteristic.ChargingState).updateValue(this.batteryState.charging);
this.batteryService.getCharacteristic(this.platform.Characteristic.StatusLowBattery).updateValue(this.batteryState.low_battery);
this.batteryService
.getCharacteristic(this.platform.Characteristic.BatteryLevel)
.on("get" /* GET */, this.getBatteryLevel.bind(this));
this.batteryService
.getCharacteristic(this.platform.Characteristic.ChargingState)
.on("get" /* GET */, this.getChargingState.bind(this));
this.batteryService
.getCharacteristic(this.platform.Characteristic.StatusLowBattery)
.on("get" /* GET */, this.getLowBatteryState.bind(this));
// set accessory information
this.somaDevice.getInfomationCharacteristics()
.then((deviceInformation) => {
this.platform.log.debug(`accessory ${this.accessory.displayName} successfully gets device information`);
this.accessory.getService(this.platform.Service.AccessoryInformation)
.setCharacteristic(this.platform.Characteristic.Manufacturer, deviceInformation.manufacturer)
.setCharacteristic(this.platform.Characteristic.Model, 'Smart Shades')
.setCharacteristic(this.platform.Characteristic.SerialNumber, deviceInformation.serial)
.setCharacteristic(this.platform.Characteristic.HardwareRevision, deviceInformation.hardwareRevision)
.setCharacteristic(this.platform.Characteristic.SoftwareRevision, deviceInformation.softwareRevision)
.setCharacteristic(this.platform.Characteristic.FirmwareRevision, deviceInformation.firmwareRevision);
void this.poll();
})
.catch((error) => this.platform.log.error(`accessory ${this.accessory.displayName} failed to get device information: ${error}`));
}
getBatteryLevel(callback) {
callback(null, this.batteryState.level);
}
getChargingState(callback) {
callback(null, this.batteryState.charging);
}
getLowBatteryState(callback) {
callback(null, this.batteryState.low_battery);
}
getPositionState(callback) {
callback(null, this.shadesState.positionState);
}
getCurrentPosition(callback) {
callback(null, this.shadesState.currentPosition);
}
getTargetPosition(callback) {
callback(null, this.shadesState.targetPosition);
}
setTargetPosition(value, callback) {
if (value === this.shadesState.currentPosition) {
this.platform.log.debug(`${this.accessory.displayName} failed to set position because shade is at ${value} already`);
callback(null);
return;
}
if (!this.lastMovementDone) {
this.platform.log.debug(`${this.accessory.displayName} failed to set position because shade is still moving`);
callback(null);
return;
}
// update our vars
this.shadesState.targetPosition = value;
// figure out our moving dynamics.
const moveUp = this.shadesState.targetPosition > this.shadesState.currentPosition;
this.shadesState.positionState = moveUp ? POSITION_STATE.INCREASING : POSITION_STATE.DECREASING;
// tell HomeKit we're on the move.
this.service.getCharacteristic(this.platform.Characteristic.PositionState).updateValue(this.shadesState.positionState);
this.platform.log.info(`moving ${this.accessory.displayName} ${moveUp ? 'up' : 'down'} to ${this.shadesState.targetPosition}`);
// move to target, but first check target position on device
// since our stored position are reversed we need to reverse them back
this.somaDevice.getTargetPosition()
.then((targetPosition) => {
if (targetPosition !== (100 - this.shadesState.targetPosition)) {
return this.somaDevice.setTargetPosition(100 - this.shadesState.targetPosition);
}
})
.then(() => {
this.platform.log.debug(`${this.accessory.displayName} successfully set target position ${this.shadesState.targetPosition}`);
callback(null);
})
.catch((error) => {
this.platform.log.error(`${this.accessory.displayName} failed to set target position: ${error}`);
callback(new Error(error));
});
}
async poll() {
// Loop forever.
for (;;) {
this.platform.log.debug(`${this.accessory.displayName} started polling`);
const initialized = await this.somaDevice.initialize().catch((error) => {
this.platform.log.error(`${this.accessory.displayName} failed to initialize: ${error}`);
return false;
});
if (!initialized) {
await this.sleep(REFRESH_RATE);
continue;
}
this.batteryState.level = await this.somaDevice.getBatteryLevel().catch((error) => {
this.platform.log.error(`${this.accessory.displayName} failed to get battery level: ${error}`);
return 0;
});
this.platform.log.debug(`setting ${this.accessory.displayName} battery level to ${this.batteryState.level}`);
this.batteryService.getCharacteristic(this.platform.Characteristic.BatteryLevel).updateValue(this.batteryState.level);
if (this.batteryState.level <= LOW_BATTERY_LEVEL) {
this.batteryState.low_battery = LOW_BATTERY.LOW;
}
else {
this.batteryState.low_battery = LOW_BATTERY.NORMAL;
}
this.batteryService.getCharacteristic(this.platform.Characteristic.StatusLowBattery).updateValue(this.batteryState.low_battery);
this.shadesState.currentPosition = 100 - (await this.somaDevice.getCurrentPosition().catch((error) => {
this.platform.log.error(`${this.accessory.displayName} failed to get current position: ${error}`);
return 100;
}));
this.platform.log.debug(`updating ${this.accessory.displayName} current position to ${this.shadesState.currentPosition}`);
this.service.getCharacteristic(this.platform.Characteristic.CurrentPosition).updateValue(this.shadesState.currentPosition);
const targetPosition = 100 - (await this.somaDevice.getTargetPosition().catch((error) => {
this.platform.log.error(`${this.accessory.displayName} failed to get target position: ${error}`);
return 100;
}));
if (this.firstPoll) {
this.shadesState.targetPosition = targetPosition;
this.platform.log.debug(`updating ${this.accessory.displayName} target position to ${this.shadesState.targetPosition}`);
this.service.getCharacteristic(this.platform.Characteristic.TargetPosition).updateValue(this.shadesState.targetPosition);
this.firstPoll = false;
}
// eslint-disable-next-line max-len
this.platform.log.debug(`${this.accessory.displayName} currentPosition ${this.shadesState.currentPosition} targetPosition ${this.shadesState.targetPosition} deviceTargetPosition ${targetPosition}`);
if (this.shadesState.positionState !== POSITION_STATE.STOPPED) {
this.platform.log.debug(`${this.accessory.displayName} is moving`);
if (this.doneMoving(this.shadesState.currentPosition, this.shadesState.targetPosition, 2)) {
this.platform.log.info(`${this.accessory.displayName} done moving, updating state`);
this.lastMovementDone = true;
this.shadesState.positionState = POSITION_STATE.STOPPED;
this.service.getCharacteristic(this.platform.Characteristic.PositionState).updateValue(this.shadesState.positionState);
}
}
this.platform.log.debug(`${this.accessory.displayName} done polling`);
// Sleep until our next update.
await this.sleep(REFRESH_RATE);
}
}
// the shades tends to move a little bit more than what we set
doneMoving(currentPosition, targetPosition, threshold) {
const lb = (targetPosition - threshold) >= 0 ? (targetPosition - threshold) : 0;
const hb = (targetPosition + threshold) <= 100 ? (targetPosition + threshold) : 100;
return currentPosition >= lb && currentPosition <= hb;
}
// Emulate a sleep function.
sleep(s) {
return new Promise(resolve => setTimeout(resolve, s * 1000));
}
}
exports.ShadesAccessory = ShadesAccessory;
//# sourceMappingURL=platformAccessory.js.map