Skip to content

Commit

Permalink
feat!: initial creation of roller shades (just shown, not yet control…
Browse files Browse the repository at this point in the history
…lable)
  • Loading branch information
apexad committed Nov 27, 2020
1 parent fdbe90a commit a71e63b
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 4 deletions.
7 changes: 6 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ export interface MySmartRollerShadesConfig {
export interface MySmartRollerShadesAuth {
username: string;
password: string;
}
}

export interface MySmartRollerShade {
id: string;
name: string;
}
90 changes: 87 additions & 3 deletions src/platform.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import rp from 'request-promise';
import jwt from 'jsonwebtoken';
import {
API,
DynamicPlatformPlugin,
Expand All @@ -9,12 +11,18 @@ import {
} from 'homebridge';
import {
PLATFORM_NAME,
// PLUGIN_NAME,
PLUGIN_NAME,
MYSMARTBLINDS_DOMAIN,
TILTSMARTHOME_OPTIONS,
TILTSMARTHOME_URL,

} from './settings';
import {
MySmartRollerShadesConfig,
MySmartRollerShadesAuth,
MySmartRollerShade,
} from './config';
import { MySmartRollerShadesAccessory } from './platformAccessory';

export class MySmartRollerShadesBridgePlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
Expand All @@ -23,6 +31,17 @@ export class MySmartRollerShadesBridgePlatform implements DynamicPlatformPlugin
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
auth!: MySmartRollerShadesAuth;
authToken!: string;
authTokenInterval?: NodeJS.Timeout;
requestOptions!: {
method: string;
uri: string;
json: boolean;
headers: {
Authorization: string;
};
};


constructor(
public readonly log: Logger,
Expand All @@ -41,10 +60,10 @@ export class MySmartRollerShadesBridgePlatform implements DynamicPlatformPlugin

try {
if (!this.config.username) {
throw new Error('MySmartBlinds Bridge - You must provide a username');
throw new Error('MySmartRollerShades Bridge - You must provide a username');
}
if (!this.config.password) {
throw new Error('MySmartBlinds Bridge - You must provide a password');
throw new Error('MySmartRollershades Bridge - You must provide a password');
}
this.auth = {
username: this.config.username,
Expand All @@ -69,7 +88,72 @@ export class MySmartRollerShadesBridgePlatform implements DynamicPlatformPlugin
this.accessories.push(accessory);
}

refreshAuthToken() {
return rp({
method: 'POST',
uri: `https://${MYSMARTBLINDS_DOMAIN}/oauth/token`,
json: true,
body: Object.assign(
{},
TILTSMARTHOME_OPTIONS,
this.auth,
),
}).then((response) => {
this.authToken = response.access_token;
this.requestOptions = {
method: 'GET',
uri: TILTSMARTHOME_URL,
json: true,
headers: { Authorization: `Bearer ${response.access_token}` },
};

if (this.config.allowDebug) {
const authTokenExpireDate = new Date((jwt.decode(response.id_token || '{ exp: 0 }') as { exp: number }).exp * 1000).toISOString();
this.log.info(`authToken refresh, now expires ${authTokenExpireDate}`);
}
});
}

discoverDevices() {
this.log.info('This plugin is still a work in progress');
this.refreshAuthToken().then(() => {
this.authTokenInterval = setInterval(this.refreshAuthToken.bind(this), 1000 * 60 * 60 * 8);
rp(this.requestOptions).then((response) => {
response.rooms.forEach((room: { rollerShades: MySmartRollerShade[] }) => {
room.rollerShades.forEach((rollerShade) => {
const {
id: shadeID,
name: shadeName,
} = rollerShade;
const uuid = this.api.hap.uuid.generate(shadeID);

const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
this.log.debug(`Restore cached roller shade: ${shadeName}`);

new MySmartRollerShadesAccessory(this, existingAccessory);

this.api.updatePlatformAccessories([existingAccessory]);
} else {
this.log.info(`Adding new roller shade: ${shadeName}`);

// create a new accessory
const accessory = new this.api.platformAccessory(shadeName, uuid);
accessory.context.shade = {
name: shadeName,
id: shadeID,
shadePosition: 100, // change to real value via get shade
batteryLevel: 100, // change to real value via get shade
};

new MySmartRollerShadesAccessory(this, accessory);

this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
});
// need to figure out how to handle deleted roller shades
});
});
});
}
}
122 changes: 122 additions & 0 deletions src/platformAccessory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
Service,
PlatformAccessory,
CharacteristicValue,
CharacteristicSetCallback,
} from 'homebridge';
// import rp from 'request-promise';
import { MySmartRollerShadesBridgePlatform } from './platform';

export class MySmartRollerShadesAccessory {
service!: Service;
batteryService: Service;
statusLog: boolean;
pollingInterval: number;
name: string;
id: string;
platform: MySmartRollerShadesBridgePlatform;
accessory: PlatformAccessory;
allowDebug: boolean;

constructor(
platform: MySmartRollerShadesBridgePlatform,
accessory: PlatformAccessory,
) {
this.platform = platform;
this.name = accessory.context.shade.name;
this.id = accessory.context.shade.id;
this.statusLog = platform.config.statusLog || false;
this.pollingInterval = platform.config.pollingInterval || 0;
this.allowDebug = platform.config.allowDebug || false;

accessory.getService(this.platform.Service.AccessoryInformation)!
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Tilt Smart Home')
.setCharacteristic(this.platform.Characteristic.Model, 'MySmartRollerShades')
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.id);

this.service = accessory.getService(this.platform.Service.WindowCovering)
|| accessory.addService(this.platform.Service.WindowCovering);

this.service.setCharacteristic(this.platform.Characteristic.Name, this.name);

this.service.getCharacteristic(this.platform.Characteristic.TargetPosition)
.on('set', this.setTargetPosition.bind(this));
this.updatePosition(accessory.context.shade.shadePosition);

this.batteryService = accessory.getService(this.platform.Service.BatteryService)
|| accessory.addService(this.platform.Service.BatteryService, `${this.name} Battery`, `${this.id} Battery`);
this.updateBattery(accessory.context.shade.batteryLevel);

this.accessory = accessory;

if (this.pollingInterval > 0) {
if (this.allowDebug) {
this.platform.log.info(`Begin polling for ${this.name}`);
}
setTimeout(() => this.refreshRollerShade(), this.pollingInterval * 1000 * 60);
}
}

updatePosition(currentPosition: number) {
let reportCurrentPosition = currentPosition;

if (reportCurrentPosition === 99) {
reportCurrentPosition = 100;
}
if (reportCurrentPosition === 1) {
reportCurrentPosition = 0;
}
if (this.statusLog) {
this.platform.log.info(`STATUS: ${this.name} updateCurrentPosition : ${reportCurrentPosition} (Actual ${currentPosition})`);
}

this.service.updateCharacteristic(this.platform.Characteristic.TargetPosition, reportCurrentPosition);
this.service.updateCharacteristic(this.platform.Characteristic.CurrentPosition, reportCurrentPosition);
this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.platform.Characteristic.PositionState.STOPPED);
}

updateBattery(batteryLevel: number) {
const {
StatusLowBattery,
} = this.platform.Characteristic;
// value of -1 means data was not sent correctly, so ignore it for now

this.batteryService.updateCharacteristic(this.platform.Characteristic.BatteryLevel, batteryLevel);
this.batteryService
.updateCharacteristic(
StatusLowBattery,
(batteryLevel < 20 && batteryLevel !== -1) ? StatusLowBattery.BATTERY_LEVEL_LOW : StatusLowBattery.BATTERY_LEVEL_NORMAL,
);
}

setTargetPosition(value: CharacteristicValue, callback: CharacteristicSetCallback) {
const targetPosition = value as number;
this.service.updateCharacteristic(this.platform.Characteristic.TargetPosition, targetPosition);

this.platform.log.info(`${this.name} setTargetPosition to ${value}`);

/* need to add update logic */
// update current position
this.updatePosition(targetPosition);

this.platform.log.info(`${this.name} currentPosition is now ${targetPosition}`);
callback(null);
}

refreshRollerShade() {
if (this.allowDebug) {
this.platform.log.info(`Refresh roller shade ${this.name}`);
}
/* work in progress
const shadeState = response.shadeState;
this.updatePosition(shadeState.position);
this.updateBattery(shadeState.batteryLevel as number);
let refreshShadeimeOut = this.pollingInterval * 1000 * 60; // convert minutes to milliseconds
if (response.headers['x-ratelimit-reset']) {
refreshShadeTimeOut = new Date(parseInt(response.headers['x-ratelimit-reset']) * 1000).getTime() - new Date().getTime();
this.platform.log.warn(`Rate Limit reached, refresh for ${this.name} delay to ${new Date(response.headers['x-ratelimit-reset'])}`);
}
setTimeout(() => this.refreshRollerShade(), refreshShadeTimeOut);
*/
}
}

0 comments on commit a71e63b

Please sign in to comment.