In the mean time I did manage to rewrite the Home Assistant YAML I use to control the Heat Pump to a Homey script.
/*
* HomeyScript - Eplucon Heatpump Control
*
* Directe bediening van de Eplucon warmtepomp
*
* Argument voorbeelden:
* 22
* indoor_temperature=22
* warm_water_active=1
* heating_active=0
* heatpump_operation_mode=2
*
* Als je alleen een getal meegeeft, gebruikt het script standaard:
* indoor_temperature=<waarde>
*/
const URL_BASE = 'https://portaal.eplucon.nl';
const EPLUCON_USERNAME = 'VUL_HIER_JE_EPLUCON_GEBRUIKERSNAAM_IN';
const EPLUCON_PASSWORD = 'VUL_HIER_JE_EPLUCON_WACHTWOORD_IN';
const MODULE_INDEX = 'VUL_HIER_JE_MODULE_INDEX_IN';
const DEFAULT_COMMAND = 'indoor_temperature';
const DEFAULT_VALUE = null;
// Lager = sneller. Als login niet betrouwbaar is, zet terug naar 2500.
const LOGIN_DELAY_MS = 1200;
const COMMANDS = {
indoor_temperature: {
code: '5701',
type: 'float',
min: 10.0,
max: 30.0,
blockType: 'module',
},
boiler_temperature: {
code: '5700',
type: 'integer',
min: 0,
max: 65,
blockType: 'menu',
},
boiler_temperature_delta: {
code: '5804',
type: 'float',
min: 5.0,
max: 65.0,
blockType: 'menu',
},
warm_water_active: {
code: '5711',
type: 'boolean',
min: 0,
max: 1,
blockType: 'module',
},
heatpump_active: {
code: '5715',
type: 'enum',
min: 0,
max: 3,
blockType: 'menu',
},
heatpump_operation_mode: {
code: '5712',
type: 'enum',
min: 1,
max: 5,
blockType: 'module',
},
heating_active: {
code: '5813',
type: 'boolean',
min: 0,
max: 1,
blockType: 'menu',
},
stop_heating_above: {
code: '5814',
type: 'float',
min: 0.0,
max: 30.0,
blockType: 'menu',
},
heating_curve_correction: {
code: '5886',
type: 'enum',
min: 0,
max: 4,
blockType: 'menu',
},
cooling_active: {
code: '5817',
type: 'boolean',
min: 0,
max: 1,
blockType: 'menu',
},
stop_passive_cooling_below: {
code: '5901',
type: 'float',
min: 0.0,
max: 35.0,
blockType: 'menu',
},
stop_active_cooling_below: {
code: '5818',
type: 'float',
min: 0.0,
max: 35.0,
blockType: 'menu',
},
};
const cookieJar = {};
async function sleep(ms) {
if (ms > 0) {
await wait(ms);
}
}
function assertConfig() {
if (!EPLUCON_USERNAME || EPLUCON_USERNAME === 'VUL_HIER_JE_EPLUCON_GEBRUIKERSNAAM_IN') {
throw new Error('Vul EPLUCON_USERNAME in.');
}
if (!EPLUCON_PASSWORD || EPLUCON_PASSWORD === 'VUL_HIER_JE_EPLUCON_WACHTWOORD_IN') {
throw new Error('Vul EPLUCON_PASSWORD in.');
}
if (!MODULE_INDEX || MODULE_INDEX === 'VUL_HIER_JE_MODULE_INDEX_IN') {
throw new Error('Vul MODULE_INDEX in.');
}
}
function getSetCookieHeaders(headers) {
if (!headers) return [];
if (typeof headers.getSetCookie === 'function') {
return headers.getSetCookie();
}
if (typeof headers.raw === 'function') {
const raw = headers.raw();
if (raw && raw['set-cookie']) return raw['set-cookie'];
}
const header = headers.get ? headers.get('set-cookie') : null;
if (!header) return [];
return header.split(/,(?=\s*[^;,]+=)/g).map(value => value.trim());
}
function storeCookies(response) {
const cookieHeaders = getSetCookieHeaders(response.headers);
for (const cookieHeader of cookieHeaders) {
const firstPart = cookieHeader.split(';')[0];
const separatorIndex = firstPart.indexOf('=');
if (separatorIndex <= 0) continue;
const name = firstPart.slice(0, separatorIndex).trim();
const value = firstPart.slice(separatorIndex + 1).trim();
if (name) {
cookieJar[name] = value;
}
}
}
function getCookieHeader() {
return Object.entries(cookieJar)
.map(([name, value]) => `${name}=${value}`)
.join('; ');
}
function getHeader(headers, name) {
if (!headers || typeof headers.get !== 'function') return null;
return headers.get(name);
}
function absoluteUrl(location) {
if (!location) return null;
if (/^https?:\/\//i.test(location)) return location;
if (location.startsWith('/')) return `${URL_BASE}${location}`;
return `${URL_BASE}/${location}`;
}
function decodeHtmlEntities(value) {
return String(value || '')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/'/g, "'")
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
}
function unescapePortalHtml(html) {
return String(html || '')
.replace(/\\"/g, '"')
.replace(/\\\//g, '/')
.replace(/\\n/g, '\n')
.replace(/\\t/g, '\t');
}
function getAttributeFromTag(tagText, attributeName) {
const normalized = unescapePortalHtml(tagText);
const patterns = [
new RegExp(`${attributeName}\\s*=\\s*"([^"]*)"`, 'i'),
new RegExp(`${attributeName}\\s*=\\s*'([^']*)'`, 'i'),
new RegExp(`${attributeName}\\s*=\\s*([^\\s>]+)`, 'i'),
];
for (const pattern of patterns) {
const match = normalized.match(pattern);
if (match && match[1] !== undefined) {
return decodeHtmlEntities(match[1]);
}
}
return null;
}
function parseInputFields(html) {
const cleanHtml = unescapePortalHtml(html);
const inputTags = cleanHtml.match(/<input\b[^>]*>/gi) || [];
const formData = {};
for (const inputTag of inputTags) {
const name = getAttributeFromTag(inputTag, 'name');
if (!name) continue;
const value = getAttributeFromTag(inputTag, 'value');
formData[name] = value !== null ? value : '';
}
return formData;
}
function formEncode(data) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(data)) {
params.append(key, value === null || value === undefined ? '' : String(value));
}
return params.toString();
}
function looksLikeLoginPage(html) {
const text = String(html || '');
return (
/<h3[^>]*>\s*Login\s*<\/h3>/i.test(text) ||
/name=["']username["']/i.test(text) ||
/name=["']password["']/i.test(text) ||
/Gebruikersnaam\s*\/\s*E-mailadres/i.test(text)
);
}
function shortText(text, length = 300) {
const value = String(text || '').replace(/\s+/g, ' ').trim();
if (value.length <= length) return value;
return `${value.slice(0, length)}...`;
}
async function request(method, url, options = {}) {
const headers = {
...(options.headers || {}),
};
const cookieHeader = getCookieHeader();
if (cookieHeader) {
headers.Cookie = cookieHeader;
}
const response = await fetch(url, {
method,
headers,
body: options.body,
redirect: options.redirect || 'follow',
});
storeCookies(response);
const text = await response.text();
return {
status: response.status,
ok: response.ok,
url: response.url,
headers: response.headers,
text,
};
}
async function getPage(url, referer = null) {
const headers = {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'User-Agent': 'HomeyScript Eplucon',
};
if (referer) {
headers.Referer = referer;
}
const response = await request('GET', url, {
headers,
});
if (!response.ok) {
throw new Error(`GET mislukt: ${url} gaf HTTP ${response.status}`);
}
return response.text;
}
async function postLoginForm(loginUrl, formData) {
const response = await request('POST', loginUrl, {
redirect: 'manual',
headers: {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Content-Type': 'application/x-www-form-urlencoded',
Origin: URL_BASE,
Referer: `${URL_BASE}/login`,
'User-Agent': 'HomeyScript Eplucon',
},
body: formEncode(formData),
});
if (response.status >= 300 && response.status < 400) {
const location = getHeader(response.headers, 'location');
const redirectUrl = absoluteUrl(location);
if (!redirectUrl) {
throw new Error('Login gaf een redirect zonder Location-header.');
}
return await getPage(redirectUrl, loginUrl);
}
if (!response.ok) {
throw new Error(`Login mislukt: HTTP ${response.status}. ${shortText(response.text)}`);
}
return response.text;
}
async function postAjaxForm(url, formData, referer) {
const response = await request('POST', url, {
redirect: 'manual',
headers: {
Accept: 'application/json, text/javascript, */*; q=0.01',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
Origin: URL_BASE,
Referer: referer || `${URL_BASE}/e-control`,
'User-Agent': 'HomeyScript Eplucon',
},
body: formEncode(formData),
});
if (response.status >= 300 && response.status < 400) {
const location = getHeader(response.headers, 'location');
const redirectUrl = absoluteUrl(location);
if (!redirectUrl) {
throw new Error('Eplucon gaf een redirect zonder Location-header.');
}
return await getPage(redirectUrl, referer || `${URL_BASE}/e-control`);
}
if (!response.ok) {
throw new Error(`Commando mislukt: HTTP ${response.status}. ${shortText(response.text)}`);
}
return response.text;
}
function parseScriptArguments() {
let command = DEFAULT_COMMAND;
let value = DEFAULT_VALUE;
if (typeof args !== 'undefined' && Array.isArray(args) && args.length > 0) {
const firstArg = String(args[0]).trim();
if (args.length >= 2) {
command = String(args[0]).trim();
value = String(args[1]).trim();
} else if (firstArg.includes('=')) {
const separatorIndex = firstArg.indexOf('=');
command = firstArg.slice(0, separatorIndex).trim();
value = firstArg.slice(separatorIndex + 1).trim();
} else {
value = firstArg;
}
}
if (!command) {
throw new Error('Geen command opgegeven.');
}
if (value === null || value === undefined || value === '') {
throw new Error('Geen waarde opgegeven. Gebruik bijvoorbeeld 22 of indoor_temperature=22.');
}
return {
command,
value,
};
}
function normalizeNumberText(value) {
return String(value).trim().replace(',', '.');
}
function validateValue(command, rawValue) {
const commandConfig = COMMANDS[command];
if (!commandConfig) {
throw new Error(`Onbekend command: ${command}`);
}
const valueText = normalizeNumberText(rawValue);
let value;
if (commandConfig.type === 'float') {
value = Number(valueText);
if (!Number.isFinite(value)) {
throw new Error(`Waarde voor ${command} moet een getal zijn.`);
}
if (value < commandConfig.min || value > commandConfig.max) {
throw new Error(
`Waarde ${value} valt buiten bereik voor ${command}: ${commandConfig.min} t/m ${commandConfig.max}.`
);
}
return String(value);
}
if (commandConfig.type === 'integer' || commandConfig.type === 'enum') {
value = Number(valueText);
if (!Number.isInteger(value)) {
throw new Error(`Waarde voor ${command} moet een geheel getal zijn.`);
}
if (value < commandConfig.min || value > commandConfig.max) {
throw new Error(
`Waarde ${value} valt buiten bereik voor ${command}: ${commandConfig.min} t/m ${commandConfig.max}.`
);
}
return String(value);
}
if (commandConfig.type === 'boolean') {
const lower = String(rawValue).trim().toLowerCase();
if (['true', 'on', 'aan', 'yes', 'ja'].includes(lower)) {
value = 1;
} else if (['false', 'off', 'uit', 'no', 'nee'].includes(lower)) {
value = 0;
} else {
value = Number(valueText);
}
if (!Number.isInteger(value) || value < 0 || value > 1) {
throw new Error(`Waarde voor ${command} moet 0 of 1 zijn.`);
}
return String(value);
}
throw new Error(`Niet ondersteund command-type: ${commandConfig.type}`);
}
function buildControlPayload(commandConfig, value) {
const typeMap = {
float: {
format: '2',
type: '2',
},
integer: {
format: '1',
type: '1',
},
boolean: {
format: '0',
type: '10',
},
enum: {
format: '0',
type: '11',
},
};
const mapped = typeMap[commandConfig.type];
if (!mapped) {
throw new Error(`Niet ondersteund command-type: ${commandConfig.type}`);
}
return JSON.stringify([
{
name: 'format',
value: mapped.format,
},
{
name: 'type',
value: mapped.type,
},
{
name: 'menutype',
value: 'MU',
},
{
name: 'blockType',
value: commandConfig.blockType,
},
{
name: 'tile_value',
value,
},
{
name: 'ido',
value: commandConfig.code,
},
]);
}
async function login() {
const loginUrl = `${URL_BASE}/login`;
const loginPageHtml = await getPage(loginUrl);
const formData = parseInputFields(loginPageHtml);
if (!formData._token) {
throw new Error('Geen CSRF-token gevonden op de loginpagina.');
}
formData.username = EPLUCON_USERNAME;
formData.password = EPLUCON_PASSWORD;
await sleep(LOGIN_DELAY_MS);
const loginResponseHtml = await postLoginForm(loginUrl, formData);
if (/inloggegevens is niet/i.test(loginResponseHtml)) {
throw new Error('Inloggen mislukt. Controleer gebruikersnaam en wachtwoord.');
}
if (looksLikeLoginPage(loginResponseHtml)) {
throw new Error(`Login niet geaccepteerd. Response: ${shortText(loginResponseHtml)}`);
}
}
async function getControlForm(commandConfig) {
const controlUrl =
`${URL_BASE}/e-control/ajax/modal/${commandConfig.code}` +
`?blockType=module&account_module_index=${encodeURIComponent(MODULE_INDEX)}`;
const response = await request('GET', controlUrl, {
headers: {
Accept: 'text/html,application/json,*/*',
'X-Requested-With': 'XMLHttpRequest',
Referer: `${URL_BASE}/e-control`,
'User-Agent': 'HomeyScript Eplucon',
},
});
if (!response.ok) {
throw new Error(`Control-formulier ophalen mislukt: HTTP ${response.status}. Controleer MODULE_INDEX.`);
}
if (looksLikeLoginPage(response.text)) {
throw new Error('Eplucon-sessie is niet actief; control-formulier kwam terug op de loginpagina.');
}
if (!response.text || response.text.trim() === '') {
throw new Error('Control-formulier is leeg. Controleer MODULE_INDEX.');
}
return parseInputFields(response.text);
}
async function sendControlData(commandConfig, value, formData) {
const sendControlUrl =
`${URL_BASE}/e-control/ajax/send_control_data` +
`?account_module_index=${encodeURIComponent(MODULE_INDEX)}`;
formData.data = buildControlPayload(commandConfig, value);
const responseText = await postAjaxForm(sendControlUrl, formData, `${URL_BASE}/e-control`);
if (looksLikeLoginPage(responseText)) {
throw new Error('Eplucon stuurde na het commando opnieuw de loginpagina terug. Commando is niet uitgevoerd.');
}
return responseText;
}
async function main() {
assertConfig();
const startedAt = Date.now();
const { command, value: rawValue } = parseScriptArguments();
const commandConfig = COMMANDS[command];
if (!commandConfig) {
throw new Error(`Ongeldig command: ${command}. Geldige commands: ${Object.keys(COMMANDS).join(', ')}`);
}
const value = validateValue(command, rawValue);
await login();
const formData = await getControlForm(commandConfig);
const responseText = await sendControlData(commandConfig, value, formData);
const accepted = String(responseText).trim() === '1';
if (!accepted) {
throw new Error(`Eplucon gaf geen bevestiging "1" terug. Response: ${shortText(responseText)}`);
}
await tag('Eplucon Laatste Commando', command);
await tag('Eplucon Laatste Waarde', value);
await tag('Eplucon Laatste Bedieningsactie', `${command}=${value}`);
await tag('Eplucon Laatste Bedieningsactie Tijd', new Date().toISOString());
return {
status: 'success',
command,
value,
accepted,
durationMs: Date.now() - startedAt,
updated: new Date().toISOString(),
};
}
return await main();