@fabienjuif/node-tado-client
Version:
A Tado client for nodejs in TS
411 lines (410 loc) • 16.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Tado = void 0;
const simple_oauth2_1 = require("simple-oauth2");
const axios_1 = __importDefault(require("axios"));
const https_1 = require("https");
__exportStar(require("./types"), exports);
const EXPIRATION_WINDOW_IN_SECONDS = 300;
const tado_auth_url = 'https://auth.tado.com';
const tado_url = 'https://my.tado.com';
const tado_config = {
client: {
id: 'tado-web-app',
secret: 'wZaRN7rpjn3FoNyF5IFuxg9uMzYJcvOoQ8QWiIqS3hfk6gLhVlG57j5YNoZL2Rtc',
},
auth: {
tokenHost: tado_auth_url,
},
};
const client = new simple_oauth2_1.ResourceOwnerPassword(tado_config);
class Tado {
constructor(username, password) {
this._username = username;
this._password = password;
this._httpsAgent = new https_1.Agent({ keepAlive: true });
}
async _login() {
if (!this._username || !this._password) {
throw new Error('Please login before using Tado!');
}
const tokenParams = {
username: this._username,
password: this._password,
scope: 'home.user',
};
try {
this._accessToken = await client.getToken(tokenParams);
}
catch (error) {
throw error;
}
}
async _refreshToken() {
if (!this._accessToken) {
await this._login();
}
if (!this._accessToken) {
throw new Error(`No access token available, even after login in.`);
}
// If the start of the window has passed, refresh the token
const shouldRefresh = this._accessToken.expired(EXPIRATION_WINDOW_IN_SECONDS);
if (shouldRefresh) {
try {
this._accessToken = await this._accessToken.refresh();
}
catch (error) {
this._accessToken = null;
await this._login();
}
}
}
async login(username, password) {
this._username = username;
this._password = password;
await this._login();
}
async apiCall(url, method = 'get', data) {
var _a;
await this._refreshToken();
let callUrl = tado_url + url;
if (url.includes('https')) {
callUrl = url;
}
const request = {
url: callUrl,
method: method,
data: data,
headers: {
Authorization: 'Bearer ' + ((_a = this._accessToken) === null || _a === void 0 ? void 0 : _a.token.access_token),
},
httpsAgent: this._httpsAgent,
};
if (method !== 'get' && method !== 'GET') {
request.data = data;
}
const response = await (0, axios_1.default)(request);
return response.data;
}
getMe() {
return this.apiCall('/api/v2/me');
}
getHome(home_id) {
return this.apiCall(`/api/v2/homes/${home_id}`);
}
getWeather(home_id) {
return this.apiCall(`/api/v2/homes/${home_id}/weather`);
}
getDevices(home_id) {
return this.apiCall(`/api/v2/homes/${home_id}/devices`);
}
getDeviceTemperatureOffset(serial_no) {
return this.apiCall(`/api/v2/devices/${serial_no}/temperatureOffset`);
}
// TODO: type
getInstallations(home_id) {
return this.apiCall(`/api/v2/homes/${home_id}/installations`);
}
getUsers(home_id) {
return this.apiCall(`/api/v2/homes/${home_id}/users`);
}
getState(home_id) {
return this.apiCall(`/api/v2/homes/${home_id}/state`);
}
getMobileDevices(home_id) {
return this.apiCall(`/api/v2/homes/${home_id}/mobileDevices`);
}
getMobileDevice(home_id, mobile_device_id) {
return this.apiCall(`/api/v2/homes/${home_id}/mobileDevices/${mobile_device_id}`);
}
getMobileDeviceSettings(home_id, mobile_device_id) {
return this.apiCall(`/api/v2/homes/${home_id}/mobileDevices/${mobile_device_id}/settings`);
}
setGeoTracking(home_id, mobile_device_id, geoTrackingEnabled) {
return this.getMobileDeviceSettings(home_id, mobile_device_id).then((settings) => this.apiCall(`/api/v2/homes/${home_id}/mobileDevices/${mobile_device_id}/settings`, 'put', {
...settings,
geoTrackingEnabled: geoTrackingEnabled,
}));
}
getZones(home_id) {
return this.apiCall(`/api/v2/homes/${home_id}/zones`);
}
getZoneState(home_id, zone_id) {
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/state`);
}
getZoneCapabilities(home_id, zone_id) {
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/capabilities`);
}
/**
* @returns an empty object if overlay does not exist
*/
getZoneOverlay(home_id, zone_id) {
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/overlay`).catch((error) => {
if (error.response.status === 404) {
return {};
}
throw error;
});
}
/**
* @param reportDate date with YYYY-MM-DD format (ex: `2022-11-12`)
*/
getZoneDayReport(home_id, zone_id, reportDate) {
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/dayReport?date=${reportDate}`);
}
getTimeTables(home_id, zone_id) {
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/schedule/activeTimetable`);
}
getAwayConfiguration(home_id, zone_id) {
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/awayConfiguration`);
}
getTimeTable(home_id, zone_id, timetable_id) {
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/schedule/timetables/${timetable_id}/blocks`);
}
clearZoneOverlay(home_id, zone_id) {
console.warn('This method of clearing zone overlays will soon be deprecated, please use clearZoneOverlays');
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/overlay`, 'delete');
}
/**
* @param temperature in celcius
* @param termination if number then duration in seconds
*/
async setZoneOverlay(home_id, zone_id, power, temperature, termination, fan_speed, ac_mode) {
console.warn('This method of setting zone overlays will soon be deprecated, please use setZoneOverlays');
const zone_state = await this.getZoneState(home_id, zone_id);
const config = {
setting: zone_state.setting,
termination: {},
};
if (power.toUpperCase() == 'ON') {
config.setting.power = 'ON';
if (config.setting.type == 'HEATING' && temperature) {
config.setting.temperature = { celsius: temperature };
}
if (config.setting.type == 'AIR_CONDITIONING') {
if (ac_mode) {
config.setting.mode = ac_mode.toUpperCase();
}
if (config.setting.mode.toLowerCase() == 'heat' ||
config.setting.mode.toLowerCase() == 'cool') {
if (temperature) {
config.setting.temperature = { celsius: temperature };
}
if (fan_speed) {
config.setting.fanLevel = fan_speed.toUpperCase();
}
}
}
}
else {
config.setting.power = 'OFF';
}
if (typeof termination === 'number') {
config.type = 'MANUAL';
config.termination.typeSkillBasedApp = 'TIMER';
config.termination.durationInSeconds = termination;
}
else if (termination === undefined) {
config.type = 'MANUAL';
config.termination.typeSkillBasedApp = 'MANUAL';
}
else if (!isNaN(parseInt(termination))) {
config.type = 'MANUAL';
config.termination.typeSkillBasedApp = 'TIMER';
config.termination.durationInSeconds = parseInt(termination);
}
else if (termination && termination.toLowerCase() == 'auto') {
// Not sure how to test this is the web app
// But seems to by a combo of 'next_time_block' and geo
config.termination.type = 'TADO_MODE';
}
else if (termination &&
termination.toLowerCase() == 'next_time_block') {
config.type = 'MANUAL';
config.termination.typeSkillBasedApp = 'NEXT_TIME_BLOCK';
}
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/overlay`, 'put', config);
}
async clearZoneOverlays(home_id, zone_ids) {
const rooms = zone_ids.join(',');
return this.apiCall(`/api/v2/homes/${home_id}/overlay?rooms=${rooms}`, 'delete');
}
/**
* @param termination if number then duration in seconds
*/
async setZoneOverlays(home_id, overlays, termination) {
let termination_config = {};
if (typeof termination === 'number') {
termination_config.typeSkillBasedApp = 'TIMER';
termination_config.durationInSeconds = termination;
}
else if (termination === undefined) {
termination_config.typeSkillBasedApp = 'MANUAL';
}
else if (!isNaN(parseInt(termination))) {
termination_config.typeSkillBasedApp = 'TIMER';
termination_config.durationInSeconds = parseInt(termination);
}
else if (termination && termination.toLowerCase() == 'auto') {
termination_config.typeSkillBasedApp = 'TADO_MODE';
}
else if (termination &&
termination.toLowerCase() == 'next_time_block') {
termination_config.typeSkillBasedApp = 'NEXT_TIME_BLOCK';
}
else {
termination_config.typeSkillBasedApp = 'MANUAL';
}
let config = {
overlays: [],
};
for (let overlay of overlays) {
const zone_state = await this.getZoneState(home_id, overlay.zone_id);
const overlay_config = {
overlay: {
setting: zone_state.setting,
termination: termination_config,
},
room: overlay.zone_id,
};
[
'power',
'mode',
'temperature',
'fanLevel',
'verticalSwing',
'horizontalSwing',
'light',
].forEach((prop) => {
if (overlay.hasOwnProperty(prop)) {
if (typeof overlay[prop] === 'string' ||
overlay[prop] instanceof String) {
overlay_config.overlay.setting[prop] = overlay[prop].toUpperCase();
}
else {
overlay_config.overlay.setting[prop] = overlay[prop];
}
}
});
config.overlays.push(overlay_config);
}
return this.apiCall(`/api/v2/homes/${home_id}/overlay`, 'post', config);
}
/**
* @param temperatureOffset in celcius
*/
setDeviceTemperatureOffset(serial_no, temperatureOffset) {
const config = {
celsius: temperatureOffset,
};
return this.apiCall(`/api/v2/devices/${serial_no}/temperatureOffset`, 'put', config);
}
identifyDevice(serial_no) {
return this.apiCall(`/api/v2/devices/${serial_no}/identify`, 'post');
}
setPresence(home_id, presence) {
const upperCasePresence = presence.toUpperCase();
if (!['HOME', 'AWAY', 'AUTO'].includes(upperCasePresence)) {
throw new Error(`Invalid presence "${upperCasePresence}" must be "HOME", "AWAY", or "AUTO"`);
}
const method = upperCasePresence == 'AUTO' ? 'delete' : 'put';
const config = {
homePresence: upperCasePresence,
};
return this.apiCall(`/api/v2/homes/${home_id}/presenceLock`, method, config);
}
async isAnyoneAtHome(home_id) {
const devices = await this.getMobileDevices(home_id);
for (const device of devices) {
if (device.settings.geoTrackingEnabled &&
device.location &&
device.location.atHome) {
return true;
}
}
return false;
}
async updatePresence(home_id) {
const [isAnyoneAtHome, presenceState] = await Promise.all([
this.isAnyoneAtHome(home_id),
this.getState(home_id),
]);
const isPresenceAtHome = presenceState.presence === 'HOME';
if (isAnyoneAtHome !== isPresenceAtHome) {
return this.setPresence(home_id, isAnyoneAtHome ? 'HOME' : 'AWAY');
}
else {
return 'already up to date';
}
}
setWindowDetection(home_id, zone_id, enabled, timeout) {
const config = {
enabled: enabled,
timeoutInSeconds: timeout,
};
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/openWindowDetection`, 'PUT', config);
}
setOpenWindowMode(home_id, zone_id, activate) {
if (activate) {
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/state/openWindow/activate`, 'POST');
}
return this.apiCall(`/api/v2/homes/${home_id}/zones/${zone_id}/state/openWindow`, 'DELETE');
}
getAirComfort(home_id) {
return this.apiCall(`/api/v2/homes/${home_id}/airComfort`);
}
async getAirComfortDetailed(home_id) {
const home = await this.getHome(home_id);
const location = `latitude=${home.geolocation.latitude}&longitude=${home.geolocation.longitude}`;
const login = `username=${this._username}&password=${this._password}`;
const resp = await (0, axios_1.default)(`https://acme.tado.com/v1/homes/${home_id}/airComfort?${location}&${login}`);
return resp.data;
}
getEnergyIQ(home_id) {
return this.apiCall(`https://energy-insights.tado.com/api/homes/${home_id}/consumption`);
}
// FIXME: not working?
getEnergyIQTariff(home_id) {
return this.apiCall(`https://energy-insights.tado.com/api/homes/${home_id}/tariff`);
}
// FIXME: not working?
updateEnergyIQTariff(home_id, unit, tariffInCents) {
if (!['m3', 'kWh'].includes(unit)) {
throw new Error(`Invalid unit "${unit}" must be "m3", or "kWh"`);
}
return this.apiCall(`https://energy-insights.tado.com/api/homes/${home_id}/tariff`, 'put', { unit: unit, tariffInCents: tariffInCents });
}
getEnergyIQMeterReadings(home_id) {
return this.apiCall(`https://energy-insights.tado.com/api/homes/${home_id}/meterReadings`);
}
/**
* @param date format `YYYY-MM-DD`
*/
addEnergyIQMeterReading(home_id, date, reading) {
return this.apiCall(`https://energy-insights.tado.com/api/homes/${home_id}/meterReadings`, 'post', { date: date, reading: reading });
}
deleteEnergyIQMeterReading(home_id, reading_id) {
return this.apiCall(`https://energy-insights.tado.com/api/homes/${home_id}/meterReadings/${reading_id}`, 'delete', {});
}
getEnergySavingsReport(home_id, year, month, countryCode) {
return this.apiCall(`https://energy-bob.tado.com/${home_id}/${year}-${month}?country=${countryCode}`);
}
}
exports.Tado = Tado;