Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for new Wyze Color Bulbs #35

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This plugin adds support for Wyze Connected Home devices to [Homebridge](https:/

## Supported Devices
- Light Bulb
- Color Bulb (Mesh Light)
- Plug
- Contact Sensor
- Motion Sensor
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "homebridge-wyze-connected-home",
"version": "0.5.1",
"version": "0.6.0",
"description": "Wyze Connected Home plugin for Homebridge",
"license": "MIT",
"main": "src/index.js",
Expand All @@ -24,6 +24,7 @@
},
"dependencies": {
"axios": "^0.19.2",
"colorsys": "^1.0.22",
"md5": "^2.2.1"
},
"devDependencies": {
Expand Down
45 changes: 43 additions & 2 deletions src/WyzeAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ module.exports = class WyzeAPI {
this.authApiKey = options.authApiKey || 'WMXHYf79Nr5gIlt3r0r7p9Tcw5bvs6BB4U8O8nGJ';
this.phoneId = options.phoneId || 'bc151f39-787b-4871-be27-5a20fd0a1937';
this.appName = options.appName || 'com.hualai.WyzeCam';
this.appVer = options.appVer || 'com.hualai.WyzeCam___2.10.72';
this.appVersion = options.appVersion || '2.10.72';
this.appVer = options.appVer || 'com.hualai.WyzeCam___2.18.44';
this.appVersion = options.appVersion || '2.18.44';
this.sc = '9f275790cab94a72bd206c8876429f3c';
this.sv = '9d74946e652647e9b6c9d59326aef104';

Expand Down Expand Up @@ -228,4 +228,45 @@ module.exports = class WyzeAPI {

return result.data;
}

async runActionList(deviceMac, deviceModel, propertyId, propertyValue) {
// Wyze Color Bulbs use a new run_action_list endpoint instead of set_property
const plist = [
{
pid: propertyId,
pvalue: String(propertyValue),
}
];
if (propertyId != "P3") {
plist.push({
pid: "P3",
pvalue: "1",
})
};
const innerList = [
{
mac: deviceMac,
plist: plist,
}
];
const actionParams = {
list: innerList,
};
const actionList = [
{
instance_id: deviceMac,
action_params: actionParams,
provider_key: deviceModel,
action_key: "set_mesh_property",
}
];
const data = {
action_list: actionList,
};
this.log.debug(`run_action_list Data Body: ${JSON.stringify(data)}`);

const result = await this.request('app/v2/auto/run_action_list', data);

return result.data;
}
};
3 changes: 3 additions & 0 deletions src/WyzeConnectedHome.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { homebridge, Accessory, UUIDGen } = require('./types');
const WyzeAPI = require('./WyzeAPI');
const WyzePlug = require('./accessories/WyzePlug');
const WyzeLight = require('./accessories/WyzeLight');
const WyzeMeshLight = require('./accessories/WyzeMeshLight');
const WyzeContactSensor = require('./accessories/WyzeContactSensor');
const WyzeMotionSensor = require('./accessories/WyzeMotionSensor');

Expand Down Expand Up @@ -118,6 +119,8 @@ module.exports = class WyzeConnectedHome {
return WyzePlug;
case 'Light':
return WyzeLight;
case 'MeshLight':
return WyzeMeshLight;
case 'ContactSensor':
return WyzeContactSensor;
case 'MotionSensor':
Expand Down
11 changes: 11 additions & 0 deletions src/accessories/WyzeAccessory.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ module.exports = class WyzeAccessory {

let response = await this.plugin.client.setProperty(this.mac, this.product_model, property, value);

this.lastTimestamp = response.ts;
} finally {
this.updating = false;
}
}
async runActionList(property, value) {
try {
this.updating = true;

let response = await this.plugin.client.runActionList(this.mac, this.product_model, property, value);

this.lastTimestamp = response.ts;
} finally {
this.updating = false;
Expand Down
186 changes: 186 additions & 0 deletions src/accessories/WyzeMeshLight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
const colorsys = require('colorsys')
const { Service, Characteristic } = require('../types');
const WyzeAccessory = require('./WyzeAccessory');

const WYZE_API_POWER_PROPERTY = 'P3';
const WYZE_API_BRIGHTNESS_PROPERTY = 'P1501';
const WYZE_API_COLOR_TEMP_PROPERTY = 'P1502';
const WYZE_API_COLOR_PROPERTY = 'P1507';

const WYZE_COLOR_TEMP_MIN = 1800;
const WYZE_COLOR_TEMP_MAX = 6500;
const HOMEKIT_COLOR_TEMP_MIN = 500;
const HOMEKIT_COLOR_TEMP_MAX = 140;

module.exports = class WyzeMeshLight extends WyzeAccessory {
constructor(plugin, homeKitAccessory) {
super(plugin, homeKitAccessory);

this.getCharacteristic(Characteristic.On).on('set', this.setOn.bind(this));
this.getCharacteristic(Characteristic.Brightness).on('set', this.setBrightness.bind(this));
this.getCharacteristic(Characteristic.ColorTemperature).on('set', this.setColorTemperature.bind(this));
this.getCharacteristic(Characteristic.Hue).on('set', this.setHue.bind(this));
this.getCharacteristic(Characteristic.Saturation).on('set', this.setSaturation.bind(this));

// Local caching of HSV color space handling separate Hue & Saturation on HomeKit
// Caching idea for handling HSV colors from:
// https://github.com/QuickSander/homebridge-http-rgb-push/blob/master/index.js
this.cache = {};
this.cacheUpdated = false;
}

async updateCharacteristics(device) {
this.getCharacteristic(Characteristic.On).updateValue(device.device_params.switch_state);

let propertyList = await this.getPropertyList();
for (let property of propertyList.data.property_list) {
switch (property.pid) {
case WYZE_API_BRIGHTNESS_PROPERTY:
this.updateBrightness(property.value);
break;

case WYZE_API_COLOR_TEMP_PROPERTY:
this.updateColorTemp(property.value);
break;

case WYZE_API_COLOR_PROPERTY:
this.updateColor(property.value);
break;
}
}
}

updateBrightness(value) {
this.getCharacteristic(Characteristic.Brightness).updateValue(value);
}

updateColorTemp(value) {
let floatValue = this._rangeToFloat(value, WYZE_COLOR_TEMP_MIN, WYZE_COLOR_TEMP_MAX);
let homeKitValue = this._floatToRange(floatValue, HOMEKIT_COLOR_TEMP_MIN, HOMEKIT_COLOR_TEMP_MAX);
this.getCharacteristic(Characteristic.ColorTemperature).updateValue(homeKitValue);
}

updateColor(value) {
// Convert a Hex color from Wyze into the HSL values recognized by HomeKit.
let hslValue = colorsys.hex2Hsv(value);
this.plugin.log.debug(`Updating color record for ${this.homeKitAccessory.context.mac} to ${value}: ${JSON.stringify(hslValue)}`)

// Update Hue
this.updateHue(hslValue.h);
this.cache.hue = hslValue.h;

// Update Saturation
this.updateSaturation(hslValue.s);
this.cache.saturation = hslValue.s;
}

updateHue(value) {
this.getCharacteristic(Characteristic.Hue).updateValue(value);
}

updateSaturation(value) {
this.getCharacteristic(Characteristic.Saturation).updateValue(value);
}

getService() {
let service = this.homeKitAccessory.getService(Service.Lightbulb);

if (!service) {
service = this.homeKitAccessory.addService(Service.Lightbulb);
}

return service;
}

getCharacteristic(characteristic) {
return this.getService().getCharacteristic(characteristic);
}

async setOn(value, callback) {
this.plugin.log.info(`Setting power for ${this.homeKitAccessory.context.mac} to ${value}`);

try {
await this.runActionList(WYZE_API_POWER_PROPERTY, (value) ? '1' : '0');
callback();
} catch (e) {
callback(e);
}
}

async setBrightness(value, callback) {
this.plugin.log.info(`Setting brightness for ${this.homeKitAccessory.context.mac} to ${value}`);

try {
await this.runActionList(WYZE_API_BRIGHTNESS_PROPERTY, value);
callback();
} catch (e) {
callback(e);
}
}

async setColorTemperature(value, callback) {
let floatValue = this._rangeToFloat(value, HOMEKIT_COLOR_TEMP_MIN, HOMEKIT_COLOR_TEMP_MAX);
let wyzeValue = this._floatToRange(floatValue, WYZE_COLOR_TEMP_MIN, WYZE_COLOR_TEMP_MAX);

this.plugin.log.info(`Setting color temperature for ${this.homeKitAccessory.context.mac} to ${value} (${wyzeValue})`);

try {
await this.runActionList(WYZE_API_COLOR_TEMP_PROPERTY, wyzeValue);
callback();
} catch (e) {
callback(e);
}
}

async setHue(value, callback) {
this.plugin.log.info(`Setting hue (color) for ${this.homeKitAccessory.context.mac} to ${value}`);
this.plugin.log.debug(`(H)S Values: ${value}, ${this.cache.saturation}`);

try {
this.cache.hue = value;
if (this.cacheUpdated) {
let hexValue = colorsys.hsv2Hex(this.cache.hue, this.cache.saturation, 100);
hexValue = hexValue.replace("#", "");
this.plugin.log.info(hexValue);

await this.runActionList(WYZE_API_COLOR_PROPERTY, hexValue);
this.cacheUpdated = false;
} else {
this.cacheUpdated = true;
}
callback();
} catch (e) {
callback(e);
}
}

async setSaturation(value, callback) {
this.plugin.log.info(`Setting saturation (color) for ${this.homeKitAccessory.context.mac} to ${value}`);
this.plugin.log.debug(`H(S) Values: ${this.cache.saturation}, ${value}`);

try {
this.cache.saturation = value;
if (this.cacheUpdated) {
let hexValue = colorsys.hsv2Hex(this.cache.hue, this.cache.saturation, 100);
hexValue = hexValue.replace("#", "");
this.plugin.log.info(hexValue);

await this.runActionList(WYZE_API_COLOR_PROPERTY, hexValue);
this.cacheUpdated = false;
} else {
this.cacheUpdated = true;
}
callback();
} catch (e) {
callback(e);
}
}

_rangeToFloat(value, min, max) {
return (value - min) / (max - min);
}

_floatToRange(value, min, max) {
return Math.round((value * (max - min)) + min);
}
};