[APP][PRO] Eplucon Ecoforest warmtepomp

I cannot do custom Icons, but try to build & publish this in the weekend:
Version 1.3.3 Changes:

  1. Fix total_active_power unit - Change from W to kW in capability definition
  2. Add operation_mode_text capability - Human-readable mode (Heating/Cooling/Off/Standby)
  3. Add flow card trigger - “When operation mode changes” with filter for specific modes

Hi Wout,

Do you think re-writing the app in Python would make the option available to not inly read but also write values to the HP?

Hi @Kenny_S It is not yet on my list to rebuild it in Python :slight_smile: And for now did not consider or researched if the API from Eplucon allows it… If that is allowed it should be possible, regardless the programming…

Being able to influence the set temperature and eg. The ability to enable or disable heating, cooling and HW would indeed be very welcome!

I am now in contact with Eplucon, to get a bit more support… That will happen and therefore -without promises- would be great to get your view on what you want to be able to change via Homey… @JaccoStraaijer and @Kenny_S you already gave some input, but would be great to get some more feedback, ideally in a use case, so what problem do you want to solve via Homey in relation to Eplucon…

Note: My contact person is on paternity leave and I am on holiday in the summer… so let’s use the summer to gather your input…

Hi Wout,

My main use case is that I currently already control the Ecoforest heat pump from Homey, but indirectly via Home Assistant. The reason I would like this functionality directly in the Homey app is to avoid depending on multiple platforms/integrations for the same device.

Concrete examples of what I currently do:

  1. When there is PV surplus, I increase the domestic hot water setpoint so the heat pump can use the excess solar energy to store energy as hot water.
  2. I change the heat pump mode between heating and cooling depending on the situation/season.
  3. I change the room/thermostat setpoint.

For me, the most important writable values would be:

  • domestic hot water setpoint;
  • room/thermostat setpoint;
  • operation mode: off / heating / cooling.

The problem I want to solve is central control. Like many Homey users, I do not want to control every device from its own separate app. I want Homey to be the central automation layer for energy optimisation, comfort and seasonal control.

One important requirement for me is that this should work locally, preferably directly over the local network/LAN and not through the Eplucon cloud. Cloud/network-based control is often too slow and not reliable enough for automations. For read-only monitoring that is less critical, but for control actions such as changing setpoints or switching heating/cooling mode, local and reliable control is very important.

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(/&quot;/g, '"')
    .replace(/&#039;/g, "'")
    .replace(/&apos;/g, "'")
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/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();

Hi Wout,

Agreed with Kenny on the types of control for the heatpump. For me the reason is that the Eplucon automation is far to limited in its functionality. At the end I would like to have the ability to control all my Eplucon devices (Solax already included, Ecoforrest is in this topic, Wanas WTW to be done) to be able to get to advanced en self learning algorithms that are very personalised and specific to my home.

imagine things like:

Using pricing setpoints to determine where excess power goes to (not ontlucht having the option to always set a higher water temperature every day at the lowest price as the Eplucon app provides, but only to do that when prices are lower than x for example)

the liberty of being able to control all Eplucon devices through their api gives us the freedom to script enhanced self learning features integrating with other existing sensors and home systems.

changed:
one more thing I would like to do: Eplucon does not provide a sensor to stop cooling when humidity gets to high (strange but true). I would like to use the homey integration to actively stop cooling when humidity runs too high.
Changed 2:
in the th-touch installer section you can set the cooling temperature ‘aanvoer’. dat would be awesome for humidity. this way you can calculate the dewpoint and influence the aanvoer temperature to be above … Far better than stopping the cooling…

super @JaccoStraaijer thanks for the input, my contact at Eplucon is still with Paternity leave so don’t expect soon solutions, but will keep you all updated once I proceed the development!

Hello Wout,

I just came to see this place. I just got an Ecoforest ecoGEO C2 2-10 kW PRO+EH with th-TOUCH installed for a month. Now looking for what is possible. I expected that I would be able to gain insight into energy consumption per category via the Api: boiler, cooling, heating, etc. It seems that I only get a monthly value, in which this distinction cannot be made. I would like this in real time.

I would also like to be able to steer smartly, on epex quarterly values, on solar power availability and to be able to set when I expect hot water or a certain temperature in the room. I use Tibber app to charge my car. He is incredibly smart. I was naïve enough to think that the eplicon would also have such functionality. I can now set two temperatures for hot water and the number of hours it works at the low temperature. Tibber works the other way around: when do you need the car. Tibber make sure the car is charged at the best time. Can also be done with boiler water in my opinion: when do you need water of which temp. In order to be able to program this ourselves, we need to be able to set the trigger points. I can’t oversee it yet.

I am very curious which direction Eplicon wants to go

Omdat ik het gevoel heb dat we hier allemaal Nederlands zijn, ga ik voor het gemak maar even in het Nederlands verder. Mocht het nodig zijn, dan switch ik wel weer naar Engels.

Eplucon is helaas behoorlijk beperkt in de mogelijkheden wat betreft aansturing. Hopelijk komt daar binnen afzienbare tijd nog verandering in, maar eerlijk gezegd heb ik daar mijn twijfels over. Om er toch zoveel mogelijk uit te halen, ben ik daarom zelf wat gaan sleutelen met Homey.

Ik heb een 3-fase energiemeter geplaatst waarmee ik het volledige energieverbruik van de warmtepomp meet. Die energiestroom splits ik vervolgens op, zodat ik kan bepalen waar de warmtepomp op dat moment mee bezig is. Op basis daarvan kan ik onder andere het maken van warm tapwater apart herkennen en volgen.

Voor het warme tapwater stuur ik vervolgens op basis van het energietarief. Op gunstige momenten boost ik de warmwatertemperatuur naar 63 °C. Op de achtergrond draait een script dat bijhoudt hoeveel energie het daadwerkelijk kost om het warme water op temperatuur te brengen. Daarbij sla ik verschillende gegevens op, zoals de start- en eindtemperatuur, start- en eindtijd, brinetemperaturen en buitentemperatuur.

Met die historische gegevens maak ik vervolgens een voorspelling voor de volgende dag. Het doel daarvan is om vooraf te bepalen in welke kwartierblokken het financieel het gunstigst is om de warmwaterboost te starten. Voorlopig werkt dit nog semi-automatisch: iedere avond om 20:30 krijg ik een overzicht met het voorgestelde moment voor de volgende dag en dat moet ik nog handmatig accepteren.

Ik ben alleen bang dat het qua slimme aansturing van de warmtepomp ook ongeveer hierbij blijft. Voor warm tapwater heb je redelijk wat vrijheid om het verbruik in de tijd te verschuiven, maar voor verwarmen en koelen vind ik dat een ander verhaal.

In de winter moet het huis uiteindelijk gewoon comfortabel warm zijn. Je kunt natuurlijk zeggen: de stroom is de komende twee uur duur, dus we verwarmen even niet. Maar afhankelijk van de woning kan de temperatuur in die periode behoorlijk teruglopen. Vervolgens moet de warmtepomp daarna weer harder of langer werken om de woning op temperatuur te krijgen. Dan is het nog maar de vraag hoeveel je onder de streep daadwerkelijk bespaart. Bij koelen geldt in principe hetzelfde verhaal.

Daarnaast vind ik dat je bij een warmtepomp ook rekening moet houden met het feit dat deze juist gebaat is bij een zo rustig en constant mogelijke bedrijfsvoering. Alleen naar de elektriciteitsprijs kijken en de warmtepomp voortdurend aan de hand daarvan proberen te sturen, hoeft energetisch dus helemaal niet de beste oplossing te zijn.

Al met al denk ik daarom dat een warmtepomp maar beperkt geschikt is om echt slim op dynamische energieprijzen te sturen. Warm tapwater is naar mijn idee een van de weinige onderdelen waarbij je redelijk eenvoudig energie kunt verschuiven zonder direct comfort in te leveren. Voor verwarmen en koelen zie ik persoonlijk veel minder speelruimte.

Uiteindelijk gaat comfort bij mij toch echt boven die paar cent die je mogelijk kunt “verdienen” door de warmtepomp nog agressiever op energieprijzen te sturen.

Dank voor je uitgebreide reactie. Kan ik goed volgen. Er is ook nog een mogelijkheid met modbus dacht ik. Heb je dat ook uitgezocht? Lokaal sturen is natuurlijk het mooiste.

De data die jij via aparte meter vergaard is er gewoon. Heel flauw dat dat niet wordt gedeeld.

Modbus heb ik nog niet goed naar gekeken, uiteindelijk is dat wel de beste optie omdat het lokaal is, je wilt natuurlijk niet afhankelijk zijn van de cloud diensten van derden. De Modbus optie is wat extra hardware nodig, niks speciaals volgens mij, maar niet direct plug-and-play.

Data beschikbaar klopt gedeeltelijk, het schijnt zo te zijn dat er tijdens chiptekorten pompen zijn uitgeleverd met andere chips, de mijne is er zo een, die laat energieverbruik per 100kWh zien, dat is natuurlijk een frequentie waar niemand iets mee kan. Vandaar dat gekozen heb dit zelf te meten.

Het is een Engels topic, maar kan vragen of ze m willen om hangen… Ik heb exact dezelfde uitdaging zoals jullie die vermelden, wil mijn verbruik zien en heb daarom in de meterkast een shelly 3 bevestigd. Nog niet geïntegreerd in de app, zit er wel aan te denken.

Had ook de versie die minder chips had, maar is kosteloos later geïmplementeerd, nu zie ik aan het eind van de maand (door firmware upgrade) het verbruik.

Wil ook het liefst lokale data, dus even op tweakers gekeken naar de Modbus integratie maar dat is nog geen gelopen race…

Voor nu stuur ik via Eplucon portal op epex, als de zonnepanelen extra produceren dan gaat de boiler naar 63 graden.

Zal na mijn vakantie wat uitgebreider met de app aan de slag gaan…