miio
Version:
Control Mi Home devices, such as Mi Robot Vacuums, Mi Air Purifiers, Mi Smart Home Gateway (Aqara) and more
287 lines (239 loc) • 6.94 kB
JavaScript
'use strict';
const { Thing, Nameable } = require('abstract-things');
const { Light, Fading, Colorable, ColorTemperature, ColorFull } = require('abstract-things/lights');
const { color } = require('abstract-things/values');
const MiioApi = require('../device');
const Power = require('./capabilities/power');
const Dimmable = require('./capabilities/dimmable');
const DEFAULT_EFFECT = 'smooth';
const DEFAULT_DURATION = 500;
const Yeelight = Thing.type(Parent => class Yeelight extends Parent
.with(Light, Fading, MiioApi, Power, Dimmable, Nameable)
{
static get type() {
return 'miio:yeelight';
}
constructor(options) {
super(options);
this.defineProperty('power', {
name: 'power',
mapper: v => v === 'on'
});
this.defineProperty('bright', {
name: 'brightness',
mapper: parseInt
});
// Used for scheduling turning off after a while
this.defineProperty('delayoff', {
name: 'offDelay',
mapper: parseInt
});
// Query for the color mode
this.defineProperty('color_mode', {
name: 'colorMode',
mapper: v => {
v = parseInt(v);
switch(v) {
case 1:
return 'rgb';
case 2:
return 'colorTemperature';
case 3:
return 'hsv';
}
}
});
// Read the name of the light
this.defineProperty('name');
/*
* Set maximum fade time to 30s - seems to work well, longer values
* sometimes cause jumps on certain models.
*/
this.updateMaxChangeTime('30s');
}
propertyUpdated(key, value) {
if(key === 'name') {
this.metadata.name = value;
}
super.propertyUpdated(key, value);
}
changePower(power) {
// TODO: Support for duration
return this.call(
'set_power',
Yeelight.withEffect(power ? 'on' : 'off'),
{
refresh: [ 'power']
}
).then(MiioApi.checkOk);
}
/**
* Make the current state the default state, meaning the light will restore
* to this state after power has been cut.
*/
setDefault() {
return this.call('set_default')
.then(MiioApi.checkOk);
}
changeBrightness(brightness, options) {
if(brightness <= 0) {
return this.changePower(false);
} else {
let promise;
if(options.powerOn && this.power() === false) {
// Currently not turned on
promise = this.changePower(true);
} else {
promise = Promise.resolve();
}
return promise.then(() =>
this.call(
'set_bright',
Yeelight.withEffect(brightness, options.duration), {
refresh: [ 'brightness' ]
})
).then(MiioApi.checkOk);
}
}
changeName(name) {
return this.call('set_name', [ name ])
.then(MiioApi.checkOk)
.then(() => this.metadata.name = name);
}
/**
* Helper method to combine an argument with effect information.
*/
static withEffect(arg, duration) {
const result = Array.isArray(arg) ? arg : [ arg ];
if(duration) {
if(duration.ms > 0) {
result.push(DEFAULT_EFFECT);
result.push(duration.ms);
} else {
result.push('sudden');
result.push(0);
}
} else {
result.push(DEFAULT_EFFECT);
result.push(DEFAULT_DURATION);
}
return result;
}
});
module.exports.Yeelight = Yeelight;
module.exports.ColorTemperature = Thing.mixin(Parent => class extends Parent
.with(MiioApi, Colorable, ColorTemperature)
{
constructor(...args) {
super(...args);
// Color temperature
this.defineProperty('ct', {
name: 'colorTemperature',
mapper: parseInt
});
// TODO: Do all Yeelights use the same color range?
this.updateColorTemperatureRange(2700, 6500);
}
propertyUpdated(key, value) {
if(key === 'colorTemperature') {
this.updateColor(color.temperature(value));
}
super.propertyUpdated(key, value);
}
changeColor(color, options) {
const range = this.colorTemperatureRange;
const temperature = Math.min(Math.max(color.temperature.kelvins, range.min), range.max);
return this.call('set_ct_abx', Yeelight.withEffect(temperature, options.duration), {
refresh: [ 'colorTemperature' ]
}).then(MiioApi.checkOk);
}
});
module.exports.ColorFull = Thing.mixin(Parent => class extends Parent
.with(MiioApi, Colorable, ColorTemperature, ColorFull)
{
constructor(...args) {
super(...args);
// Color temperature
this.defineProperty('ct', {
name: 'colorTemperature',
mapper: parseInt
});
this.defineProperty('rgb', {
name: 'colorRGB',
mapper: rgb => {
rgb = parseInt(rgb);
return {
red: (rgb >> 16) & 0xff,
green: (rgb >> 8) & 0xff,
blue: rgb & 0xff
};
}
});
this.defineProperty('hue', {
name: 'colorHue',
mapper: parseInt
});
this.defineProperty('sat', {
name: 'colorSaturation',
mapper: parseInt
});
this.metadata.addCapabilities('color:temperature', 'color:full');
// TODO: Do all Yeelights use the same color range?
this.updateColorTemperatureRange(2700, 6500);
}
propertyUpdated(key, value) {
if(key === 'colorTemperature' || key === 'colorMode' || key === 'colorRGB' || key === 'colorHue' || key === 'colorSaturation' || key === 'brightness') {
let currentColor = this.color();
switch(this.property('colorMode')) {
case 'colorTemperature':
// Currently using color temperature mode, parse as temperature
currentColor = color.temperature(this.property('colorTemperature'));
break;
case 'rgb': {
// Using RGB, parse if we have gotten the RGB value
let rgb = this.property('colorRGB');
if(rgb) {
currentColor = color.rgb(rgb.red, rgb.green, rgb.blue);
}
break;
}
case 'hsv': {
// Using HSV, parse the hue, saturation and get the brightness to set color
let hue = this.property('colorHue');
let saturation = this.property('colorSaturation');
if(typeof hue !== 'undefined' && typeof saturation !== 'undefined') {
currentColor = color.hsv(hue, saturation, this.property('brightness'));
}
}
}
this.updateColor(currentColor);
}
super.propertyUpdated(key, value);
}
changeColor(color, options) {
if(color.is('temperature')) {
// The user has request a color via temperature
const range = this.colorTemperatureRange;
const temperature = Math.min(Math.max(color.temperature.kelvins, range.min), range.max);
return this.call('set_ct_abx', Yeelight.withEffect(temperature, options.duration), {
refresh: [ 'colorTemperature', 'colorMode' ]
}).then(MiioApi.checkOk);
} else if(color.is('hsl')) {
/*
* User has requested hue and saturation
*/
return this.call('set_hsv', Yeelight.withEffect([ color.hue, color.saturation ], options.duration), {
refresh: [ 'colorHue', 'colorSaturation', 'colorMode' ]
}).then(MiioApi.checkOk);
} else {
/*
* Fallback to applying via RGB.
*/
color = color.rgb;
const rgb = color.red * 65536 + color.green * 256 + color.blue;
return this.call('set_rgb', Yeelight.withEffect(rgb, options.duration), {
refresh: [ 'colorRGB', 'colorMode' ]
}).then(MiioApi.checkOk);
}
}
});