kasa-smart-hub
Version:
Virtual Smart Hub for TP-Link Kasa Smart Home
159 lines (148 loc) • 5.11 kB
JavaScript
import assert from 'assert';
import Joi from 'joi-strict';
import tplink from 'tplink-smarthome-api';
import axios from '@blackflux/axios';
import configSchema from '../resources/config-schema.js';
import computeLinks from '../util/compute-links.js';
import secondsToHumansReadable from '../util/seconds-to-human-readable.js';
import computeDelay from '../util/compute-delay.js';
import onlyOnce from '../util/only-once.js';
import ForEach from '../util/for-each.js';
import Log from '../util/log.js';
import Apply from '../util/apply.js';
import aqiToColor from '../util/aqi-to-color.js';
import sensorToAqi from '../util/sensor-to-aqi.js';
import rgbToHsb from '../util/colors/rgb-to-hsb.js';
import hexToRgb from '../util/colors/hex-to-rgb.js';
import { ERROR_COLOR } from '../resources/config.js';
const { Client } = tplink;
export default (config_) => {
const config = {
discoveryConfig: {},
...config_
};
Joi.assert(config, configSchema);
const links = computeLinks(config);
const log = Log(config);
const client = new Client();
const forEach = ForEach(client);
const apply = Apply(log);
const registerDeviceColorUpdate = (device) => {
const provider = config?.color?.[device.alias];
if (provider === undefined) {
return;
}
assert(!device.color_update_timer);
const fn = async () => {
let hex = ERROR_COLOR;
try {
const { source } = provider;
const delay = source.interval * 1000;
assert(source.name = 'purpleair');
const now = new Date() / 1;
if (device.last_color_update && device.last_color_update + delay > now) {
return;
}
// eslint-disable-next-line no-param-reassign
device.last_color_update = now;
const { data } = await axios({
url: `https://api.purpleair.com/v1/sensors/${source.sensor}`,
method: 'GET',
headers: {
'X-API-Key': source.apiKey
},
params: {
fields: 'pm2.5_10minute,pm10.0'
}
});
const aqi = sensorToAqi({
'pm2.5': data?.sensor?.stats?.['pm2.5_10minute'],
'pm10.0': data?.sensor?.['pm10.0']
});
hex = aqiToColor(aqi);
} catch { /* ignored */ }
const [r, g, b] = hexToRgb(hex);
const [h, s, v] = rgbToHsb(r, g, b);
log('debug', `Color Update: ${hex}`);
await apply(device, 'lighting.setLightState', {
hue: h,
saturation: s,
brightness: v
});
};
fn();
// eslint-disable-next-line no-param-reassign
device.color_update_timer = setInterval(fn, 1000);
};
const updateDeviceTimer = async (device, state) => onlyOnce(`update-timer: ${device.alias}`, async () => {
const delay = computeDelay(device.alias, state, config);
if (delay === 0) {
return;
}
const rules = await apply(device, 'timer.getRules');
if (rules.err_code !== 0) {
return;
}
if (rules.rule_list.some((r) => r.enable === 1 && (r.remain - delay) < 10)) {
return;
}
const newState = await apply(device, 'getPowerState');
if (newState === state) {
log(`Timer Started: ${device.alias} - ${state ? 'OFF' : 'ON'} in ${secondsToHumansReadable(delay)}`);
await apply(device, 'timer.addRule', { delay, powerState: !state });
}
});
const onDevicePowerStateChange = async (device, state) => {
log('debug', `State Changed: ${device.alias} @ ${state ? 'on' : 'off'}`);
if (device.alias in links) {
const group = links[device.alias];
await onlyOnce(
`power-toggle: ${[device.alias, ...group].sort().join(' || ')}`,
async () => {
log(`Link Triggered: ${device.alias} -> ${[...group].join(', ')} @ ${state ? 'on' : 'off'}`);
await forEach(
(d) => d.status === 'online' && group.has(d.alias),
(d) => apply(d, 'setPowerState', state)
);
}
);
}
};
client.on('device-new', (device) => {
log(`New Device: ${device.alias}`);
device.addListener('power-on', () => onDevicePowerStateChange(device, true));
device.addListener('power-off', () => onDevicePowerStateChange(device, false));
device.addListener('power-update', async (state) => {
await updateDeviceTimer(device, state);
});
registerDeviceColorUpdate(device);
// fast polling for linked devices
if (device.alias in links) {
device.startPolling(500);
}
});
return {
start: () => {
client.startDiscovery({
broadcast: '192.168.0.255',
port: 56888,
devicesUseDiscoveryPort: true,
breakoutChildren: true,
discoveryInterval: 10000,
discoveryTimeout: 0,
offlineTolerance: 3,
...config.discoveryConfig
});
},
stop: () => {
[...client.devices.values()].forEach((d) => {
if (d.color_update_timer) {
clearInterval(d.color_update_timer);
}
d.stopPolling();
});
client.stopDiscovery();
},
getDevices: () => [...client.devices.values()]
};
};