iobroker.lovelace
Version:
With this adapter you can build visualization for ioBroker with Home Assistant Lovelace UI
485 lines (447 loc) • 21.9 kB
JavaScript
const processSensors = require('./sensor');
const utils = require('./utils');
const adapterData = require('./../dataSingleton');
const typeDetector = require('iobroker.type-detector');
// - climate =>
// STATE on/off,
// attributes: [
//
// hvac_mode: HvacMode;
// hvac_modes: HvacMode[];
// hvac_action?: HvacAction;
//
//
// current_temperature: number, <- reads temperature
// <- those all set temperature. Low & high are there to set target range.
// target_temp_step: number,
// target_temp_low: number,
// target_temp_high: number,
// min_temp: number,
// max_temp: number,
// temperature: number
// current_humidity, <- read humidity
// <- set target humidity (and range)
// humidity?: number;
// target_humidity_low?: number;
// target_humidity_high?: number;
// min_humidity?: number;
// max_humidity?: number;
// fan_mode?: string;
// fan_modes?: string[];
// preset_mode?: string;
// preset_modes?: string[];
// swing_mode?: string;
// swing_modes?: string[];
// aux_heat?: "on" | "off";
// ],
//export type HvacMode =
// | "off"
// | "heat"
// | "cool"
// | "heat_cool"
// | "auto"
// | "dry"
// | "fan_only";
// auto: 1,
// heat_cool: 2,
// heat: 3,
// cool: 4,
// dry: 5,
// fan_only: 6,
// off: 7,
// reports state?
// export type HvacAction = "off" | "heating" | "cooling" | "drying" | "idle";
// Build in presets:
// ECO Device is running an energy-saving mode
// AWAY Device is in away mode
// BOOST Device turn all valve full up
// COMFORT Device is in comfort mode
// HOME Device is in home mode
// SLEEP Device is prepared for sleep
// ACTIVITY Device is reacting to activity (e.g. movement sensors)
// Supported Features:
const CLIMATE_SUPPORT_TARGET_TEMPERATURE = 1;
//const CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE = 2;
//const CLIMATE_SUPPORT_TARGET_HUMIDITY = 4;
const CLIMATE_SUPPORT_FAN_MODE = 8;
const CLIMATE_SUPPORT_PRESET_MODE = 16;
const CLIMATE_SUPPORT_SWING_MODE = 32;
//const CLIMATE_SUPPORT_AUX_HEAT = 64;
exports.supportedFlags = {
CLIMATE_SUPPORT_TARGET_TEMPERATURE,
CLIMATE_SUPPORT_FAN_MODE,
CLIMATE_SUPPORT_PRESET_MODE,
CLIMATE_SUPPORT_SWING_MODE
};
// commands:
//
exports.processThermostatOrAirConditioning = function (id, control, name, room, func, _obj, objects, forcedEntityId) {
const entity = utils.processCommon(name, room, func, _obj, 'climate', forcedEntityId);
const states = {};
let entityTemp;
let entityHum;
const tempId = 'sensor.' + entity.entity_id.split('.')[1] + '_Temperature';
const humId = 'sensor.' + entity.entity_id.split('.')[1] + '_Humidity';
for (const state of control.states) {
if (state && state.id) {
switch(state.name) {
case 'SET': //target temperature -> required.
states.temperature = state.id;
break;
case 'MODE': //required for AC
states.hvac_mode = state.id; //will also set state, if no Power.
break;
case 'POWER':
states.state = state.id; //will also set hvac_mode if no mode.
break;
case 'HUMIDITY': //humidity:
states.current_humidity = state.id;
entityHum = processSensors.createHumiditySensor(state.id, name, func, room, _obj,objects, humId);
break;
case 'ACTUAL':
//current temperature:
//-> also detect sensor entity for temperature
states.current_temperature = state.id;
entityTemp = processSensors.createTemperatureSensor(state.id, name, func, room, _obj, objects, tempId);
break;
case 'BOOST':
states.preset_mode = state.id;
states.boost = state.id;
break;
case 'PARTY':
states.preset_mode = state.id;
states.party = state.id;
break;
case 'SPEED': //-> fan mode
states.fan_mode = state.id;
break;
case 'SWING':
states.swing_mode = state.id;
break;
default:
if (!['WORKING', 'UNREACH', 'LOWBAT', 'MAINTAIN', 'ERROR'].includes(state.name)) {
adapterData.log.info(`Unknown state ${state.name} while creating climate entity for ${id}. Please report.`);
}
}
}
}
fillClimateEntityFromStates(states, objects, entity, control.type);
return [entity, entityHum, entityTemp];
};
/**
* Create manual climate entity.
* @param id - id of "main" object, i.e. state.
* @param obj - iobroker object of id param
* @param entity - already created entity
* @param objects - id object cache
* @param custom - custom part of object
* @returns {Promise<[entity]>}
*/
exports.processManualEntity = async function(id, obj, entity, objects, custom) {
const states = custom.states || {
temperature: id
};
fillClimateEntityFromStates(states, objects, entity);
return [entity];
};
/**
* Implement attribue and state parsing for climate entities.
* @param states
* @param objects
* @param entity
* @param iobType
*/
function fillClimateEntityFromStates(states, objects, entity, iobType) {
utils.fillEntityFromStates(states, entity);
entity.attributes.supported_features = CLIMATE_SUPPORT_TARGET_TEMPERATURE;
entity.context.COMMANDS = [];
//add set_hvac_mode command -> used by lovelace to control mode & on/off.
if (states.state || states.hvac_mode) {
entity.context.COMMANDS.push({
service: 'set_hvac_mode',
setId: states.hvac_mode || states.state,
parseCommand: async (entity, command, data, user) => {
const hvac_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'hvac_mode');
const value = data.service_data.hvac_mode;
if (entity.context.STATE.setId) { //on & off is done with power state in ioBroker
await adapterData.adapter.setForeignStateAsync(entity.context.STATE.setId, value !== 'off', false, {user});
}
if (hvac_attr) {
const target = hvac_attr.lovelaceToIob[value];
if (target || target === 0) { //allow 0 but do not set mode if mode was not part of states!
await adapterData.adapter.setForeignStateAsync(hvac_attr.setId, target, false, {user});
}
}
entity.attributes.hvac_action = undefined;
}
});
}
//displays temperature.
if (states.current_temperature) {
const obj = objects[states.current_temperature];
if (obj && obj.common && obj.common.unit) {
entity.attributes.unit_of_measurement = obj.common.unit;
}
}
//controls hvac_mode which can be 'off' but not 'on', so translate 'on' to heat / cool depending on type.
if (states.state || states.stateRead) {
if (!states.hvac_mode) {
if (iobType === typeDetector.Types.airCondition) {
entity.attributes.hvac_modes = ['off', 'cool'];
} else {
entity.attributes.hvac_modes = ['off', 'heat'];
}
}
entity.context.STATE.getParser = function (entity, attr, state) {
state = state || {val: null};
entity.context.iobPower = state.val;
const target = state.val ? (iobType === typeDetector.Types.airCondition ? 'cool' : 'heat') : 'off';
const hvac_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'hvac_mode');
if (hvac_attr) {
if (!state.val) {
entity.attributes.hvac_mode = 'off'; //overwrite mode only if is 'off' or should be set to 'off'.
} else {
if (entity.context.iobMode !== undefined) {
entity.attributes.hvac_mode = hvac_attr.iobToLovelace[entity.context.iobMode] || entity.context.iobMode;
} else {
adapterData.log.warn(`No mode for ${entity.entity_id} received, yet. Asking database. Will delay update.`);
//never did get iobMode?? -> retrieve.
adapterData.adapter.getForeignState(hvac_attr.getId, s => {
const val = s ? s.val : null;
if (entity.context.iobMode === undefined) {
entity.context.iobMode = val;
const target = hvac_attr.iobToLovelace[val] || val || (iobType === typeDetector.Types.airCondition ? 'cool' : 'heat');
entity.state = target;
entity.attributes.hvac_mode = target;
}
});
}
}
} else {
entity.attributes.hvac_mode = target;
}
entity.state = entity.attributes.hvac_mode;
entity.attributes.hvac_action = undefined;
};
entity.context.STATE.historyParser = (id, val) => {
return val ? iobType === typeDetector.Types.airCondition ? 'cool' : 'heat' : 'off';
};
}
//mode is main setting of operation!!
if (states.hvac_mode) {
//iob default modes for AC: {0: 'OFF', 1: 'AUTO', 2: 'COOL', 3: 'HEAT', 4: 'ECO', 5: 'FAN_ONLY', 6: 'DRY'} -> matches quite well.
//iob default modes for thermostat: {0: 'AUTO', 1: 'MANUAL'} -> does not match so well.. hm
const hvac_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'hvac_mode');
hvac_attr.setId = states.hvac_mode;
const obj = objects[hvac_attr.getId];
if (obj && obj.common && obj.common.states) {
entity.attributes.hvac_modes = entity.attributes.hvac_modes || [];
//create translation of modes:
hvac_attr.iobToLovelace = obj.common.states;
hvac_attr.lovelaceToIob = {};
for (const key of Object.keys(obj.common.states)) {
const mode = obj.common.states[key];
entity.attributes.hvac_modes.push(mode.toLowerCase());
hvac_attr.lovelaceToIob[mode.toLowerCase()] = parseInt(key, 10); //need number!
hvac_attr.iobToLovelace[key] = mode.toLowerCase(); //make sure case is right, here.
}
} else {
//we don't know anything -> use lovelace default..
hvac_attr.lovelaceToIob = {'auto': 1, 'heat_cool': 2, 'heat': 3, 'cool': 4, 'dry': 5, 'fan_only': 6, 'off': 7};
hvac_attr.iobToLovelace = {1: 'auto', 2: 'heat_cool', 3: 'heat', 4: 'cool', 5: 'dry', 6: 'fan_only', 7: 'off'};
entity.attributes.hvac_modes = ['auto', 'heat_cool', 'heat', 'cool', 'dry', 'fan_only', 'off'];
}
if ((states.state || states.stateRead) && !entity.attributes.hvac_modes.includes('off')) {
entity.attributes.hvac_modes.push('off');
}
hvac_attr.getParser = function (entity, attr, state) {
state = state || {val: null};
entity.context.iobMode = state.val;
entity.attributes.hvac_mode = attr.iobToLovelace[state.val] || state.val;
if (!state.val && ((states.state || states.stateRead) && entity.state === 'off')) {
entity.attributes.hvac_mode = entity.state;
} else {
entity.state = entity.attributes.hvac_mode;
}
entity.attributes.hvac_action = undefined;
};
hvac_attr.historyParser = (id, val) => {
return hvac_attr.iobToLovelace[val] || val;
};
}
//preset mode:
if (states.preset_mode) {
entity.attributes.supported_features |= CLIMATE_SUPPORT_PRESET_MODE;
//we have either boost or party -> set them as presets.
entity.attributes.preset_mode = 'none';
entity.attributes.preset_modes = ['none'];
const boost_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'boost');
if (boost_attr) {
entity.attributes.preset_modes.push('boost');
boost_attr.getParser = (entity, attr, state) => {
const val = state ? state.val : null;
entity.attributes.boost = val ? 'on' : 'off';
entity.attributes.preset_mode = val ? 'boost' : entity.attributes.party === 'on' ? 'party' : 'none';
};
boost_attr.historyParser = (id, val) => val ? 'boost' : 'none';
}
const party_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'party');
if (party_attr) {
entity.attributes.preset_modes.push('party');
party_attr.getParser = (entity, attr, state) => {
const val = state ? state.val : null;
entity.attributes.party = val ? 'on' : 'off';
entity.attributes.preset_mode = val ? 'party' : entity.attributes.boost === 'on' ? 'boost' : 'none';
};
party_attr.historyParser = (id, val) => val ? 'party' : 'none';
}
const preset_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'preset_mode');
if (preset_attr) {
preset_attr.getId = undefined; //prevent direct preset_mode updates.
}
entity.context.COMMANDS.push({
service: 'set_preset_mode',
parseCommand: async (entity, command, data, user) => {
const preset = data.service_data.preset_mode;
let boostVal = false;
let partyVal = false;
if (preset === 'boost') {
boostVal = true;
} else if (preset === 'party') {
partyVal = true;
}
if (party_attr) {
await adapterData.adapter.setForeignStateAsync(party_attr.getId, partyVal, false, {user});
}
if (boost_attr) {
await adapterData.adapter.setForeignStateAsync(boost_attr.getId, boostVal, false, {user});
}
}
});
}
//swing: iob defaultStates: {0: 'AUTO', 1: 'HORIZONTAL', 2: 'STATIONARY', 3: 'VERTICAL'}
// lovelace just presents modes in drop down -> show translated values.
if (states.swing_mode) {
entity.attributes.supported_features |= CLIMATE_SUPPORT_SWING_MODE;
const swing_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'swing_mode');
const swing_obj = objects[swing_attr.getId];
if (swing_obj.common && swing_obj.common.type === 'boolean') {
swing_attr.isBoolean = true;
swing_attr.states = {false: 'off', true: 'on'};
entity.attributes.swing_modes = ['off', 'on'];
} else {
if (swing_obj && swing_obj.common && swing_obj.common.states) {
swing_attr.states = swing_obj.common.states;
entity.attributes.swing_modes = Object.values(swing_attr.states);
} else {
swing_attr.states = {}; //as we don't know a translation, prevent errors later on.
entity.attributes.swing_modes = [0, 1, 2, 3, 4, 5, 6, 7]; //add some modes to dropdown...
}
}
swing_attr.getParser = (entity, attr, state) => {
const val = state ? state.val : null;
entity.attributes.swing_mode = attr.states[val] !== undefined ? attr.states[val] : val;
};
swing_attr.historyParser = (id, val) => swing_attr.states[val] || val;
entity.context.COMMANDS.push({
service: 'set_swing_mode',
parseCommand: async (entity, command, data, user) => {
const mode = data.service_data.swing_mode;
let val;
if (swing_attr.isBoolean) {
val = mode === 'on';
} else {
val = parseInt(mode, 10);
for (const key of Object.keys(swing_attr.states)) {
if (swing_attr.states[key] === mode) {
val = parseInt(key, 10);
}
}
}
await adapterData.adapter.setForeignStateAsync(swing_attr.getId, val, false, {user});
}
});
}
//fan_mode: iob default states {0: 'AUTO', 1: 'HIGH', 2: 'LOW', 3: 'MEDIUM', 4: 'QUIET', 5: 'TURBO'},
//lovace translated modes: "off", "on", "auto" -> others will just be added as is.. hm
if (states.fan_mode) {
entity.attributes.supported_features |= CLIMATE_SUPPORT_FAN_MODE;
const fan_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'fan_mode');
const fan_obj = objects[fan_attr.getId];
if (fan_obj && fan_obj.common && fan_obj.common.states) {
fan_attr.states = fan_obj.common.states;
entity.attributes.fan_modes = Object.values(fan_attr.states);
} else {
fan_attr.states = {}; //we don't know a thing..
entity.attributes.fan_modes = [0, 1, 2, 3, 4, 5, 6, 7]; //add some modes to dropdown...
}
fan_attr.getParser = (entity, attr, state) => {
const val = state ? state.val : null;
entity.attributes.fan_mode = attr.states[val] !== undefined ? attr.states[val] : val;
};
fan_attr.historyParser = (id, val) => fan_attr.states[val] || val;
entity.context.COMMANDS.push({
service: 'set_fan_mode',
parseCommand: async (entity, command, data, user) => {
const mode = data.service_data.fan_mode;
let val = parseInt(mode, 10);
for (const key of Object.keys(fan_attr.states)) {
if (fan_attr.states[key] === mode) {
val = parseInt(key, 10);
}
}
await adapterData.adapter.setForeignStateAsync(fan_attr.getId, val, false, {user});
}
});
}
//hvac_action -> reports status, i.e. is heating in automatic mode
if (states.hvac_action) {
const obj = objects[states.hvac_action];
const attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'hvac_action');
const type = obj ? obj.common ? obj.common.type ? obj.common.type : 'string' : 'string' : 'string';
if (obj && obj.common) {
if (type === 'number' && obj.common.states) {
attr.states = obj.common.states;
}
}
attr.getParser = (entity, attr, state) => {
const val = state ? state.val : null;
if (type === 'number' && attr.states) {
entity.attributes.hvac_action = attr.historyParser(attr.getId, val);
} else if (type === 'string') {
entity.attributes.hvac_action = val;
} else {
entity.attributes.hvac_action = val ? iobType === typeDetector.Types.thermostat ? 'heating' : 'cooling' : 'idle';
}
};
attr.historyParser = (id, val) => { return attr.states ? (attr.states[val] || val) : val; };
}
//defaults:
entity.attributes.min_temp = 7;
entity.attributes.max_temp = 35;
entity.attributes.target_temp_step = 1;
entity.attributes.min_humidity = 30;
entity.attributes.max_humidity = 99;
//try to get settings from temperature state:
if (states.temperature) {
entity.context.COMMANDS.push({
service: 'set_temperature',
setId: states.temperature,
parseCommand: async (entity, command, data, user) => {
if (data.service_data.temperature === undefined) {
adapterData.log.warn(`No temperature in service call for ${entity.entity_id}. Range not yet supported.`);
}
//works only if no temperature range is supported!
await adapterData.adapter.setForeignStateAsync(command.setId, data.service_data.temperature, false, {user});
}
});
if (objects[states.temperature] && objects[states.temperature].common) {
if (!entity.attributes.unit_of_measurement && objects[states.temperature].common.unit) {
entity.attributes.unit_of_measurement = objects[states.temperature].common.unit;
}
entity.attributes.min_temp = objects[states.temperature].common.min || 7;
entity.attributes.max_temp = objects[states.temperature].common.max || 35;
entity.attributes.target_temp_step = objects[states.temperature].common.step || 1;
}
}
}