Set up house heating using Eurotronic Spirit actuators with PID control and "manual" control

This will be a tutorial on how to set up heaters using Eurotronic Spirit actuators with PID regulator forum / documentation link.

For tldr; go to Current implementation section

Let me first share a bit of history why it ended like below. It was November 2018 when I first thought - “My wife is fiddling with regulators whole winter. It would be nice to have automation doing it.” (I’m not so sure I would start that jurney again :wink: )

So I read a bit and thought it’s doable (winter is comming). I decided to go with:

  • RaspberryPi 3B+
  • Aeotec z-stick gen5
  • Eurotronic Spirit (I definitely don’t recommend those as “out the box solution” but I don’t have an alternative - suggestions are welcomed in comments)

So the first winter came. I spent hours configuring it and my wife told me to throw it out of the window and go back to manual ones (really).

Why?

  1. Constant Node disconnections
  2. Heaters going into installation mode
  3. Constant overheating the rooms

I was also really dissapointed but also stubborn and wanted to fix it.

It was easy to solve the first problem I thought one Z-wave stick is not handling my flat. Especially that the nodes are running on batteries so I bought two NEO Coolcam Zwave Outlets and set those up on 2 sides of my apartment. As those were constantly ON it worked and one problem was solved.

The second problem is still ongoing - one thing I learned is that Spirits need actual alkaline batteries. Those will not run properly on rechargeable ones (I tried Eneloop AA). So I limited the problem but 1 heater is still constantly ending up in installation mode. This is causing room overheating as it’s fully open when it’s happening (looking for any advice).

Solving the third one is a journey I think I didn’t finish yet. At first I thought well maybe the built-in thermometer is not doing its job. I bought another piece of equipment:

  1. Xiaomi Gateway
  2. Aquara temperature sensors

I set those up as external temp controls for Spirits. I had some more confidence in the measured temperature now. The heaters are next to windows so I figured out that the reported temperature by those was lover (even by 1.5 °C) compared to the actual temp in the room.

Setting up external sensors took time, money and didn’t really solve the problem.

The real problem was elsewhere. It was the built-in heating curve of the regulator. It was terribly slow. It was taking 2h-4h to shut down completely the actuator when the temperature was reached. Because of that issue room temperature was still growing as it was closing like 1% per 3 min (figured it out thanks to Influx + Grafana). In my case changing temperature at 20:00 to be like 19°C (while room was 22°C) - Spirit was fully closed ~1AM. Kind of insane.

I and many others using Spirits knew it would be best to control the valve open manually but at the beginning, it wasn’t implemented in Openhab. Second problem with controlling those was I didn’t have any way to predict what should be the valve state.

I started to play with the temperatures and set like 28°C in the morning to max out valve open and then 18°C to shut it down or even shutting those down (OFF command) to force close the valve.

The ultimate solution - PID controller.
I started to look at how to implement it myself and then all of a sudden @fwolter solution showed up.

I was able to change the Spirits mode to be manufacture specific and there would be the ability to set 0-100% of the valve instead providing desired temperature. This is what the below code will do.

Enough talking just tell me how to do it.

Current implementation

I used an abstraction layer to divide physical devices from the PID usage solution.

So in the room we physically have:

  • 1 temp sensor in the heater
  • 1 external temp sensor
  • 1 valve that we need to control (dimmer)
  • a possibility to set temperature manually on the heater
  • a possibility to set temperature by schedule

Then I have a Virtual device:

  • virtual room temperature calculated based on 2 sensors
  • desired temperature from user or schedule (SetPoint)
  • calculated dimmer state based on PID calculation
  • PID controller feature

Let’s start with code calculating the Virtual Temp

In my solution will always pick Xiaomi over the heater one. It could be possible to sum and divide but that’s not my preference. If Xiaomi is unavailable I’ll take heater one just to get things running.

Problems: when Xiaomi goes offline it was setting up temp as 100 °C
If I have to pick heater one just add 1 °C to be more realistic.

Code:

/* Calculate virtual temperature based on external sensor and one built in heater */
/* Temp priority is set to external one as it has better readings - one could divide value to get avg  */
/* Sensor in heater is close to window add + 1°C to read value because of it*/

var Room, Heater_Name,Temp_Sensor_Name
var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

Room = 'Gabinet';
Heater_Name = 'ZWaveNodeGabinet';
Temperature_Sensor_Name = 'XiaomiAqaraGabinet';


/* ------------   Common (copy/paste)  ----------------- */

var Temp_Sensor_Property_Name, ThermostatTemp_Name, VirtualTemp_Name
var SensorTemp,ThermostatTemp, FinalTemp

Temp_Sensor_Property_Name = Temperature_Sensor_Name + '_Temperature';
ThermostatTemp_Name = Heater_Name + '_Currenttemperature';
VirtualTemp_Name = Room + '_Virtual_Temperature';

/* Get temperature */
SensorTemp = itemRegistry.getItem(Temp_Sensor_Property_Name).getState();
ThermostatTemp = itemRegistry.getItem(ThermostatTemp_Name).getState();

logger.info(([Room, ' | SensorTemp: ', SensorTemp, ' | ', 'Thermostat: ', ThermostatTemp].join('')));

/* If external sensor has problem pick heater temp */
/* Xiaomi has problem -> when battery will die it reports 100 °C */

if (SensorTemp != 'NULL') {
    
    if (SensorTemp != 100){
      FinalTemp = SensorTemp;
    } else {
      logger.warn('Xiaomi Sensor Reports wrong temp!');
      FinalTemp = ThermostatTemp + 1;
    }
} else {
    FinalTemp = ThermostatTemp + 1;
}

logger.info((Room + ' | Final Calculated Temp: ' + String(FinalTemp)));
events.sendCommand(String(VirtualTemp_Name), FinalTemp);


The rule should run every minute to update Virtual temp:

Setting up a PID calculation is fairly straightforward. Figuring out the values so the solution would be fast and realiable is not (I simply reused some values others provided). PID can calculate values from plus infinite to minus infinite.

In my solution:

  • value above 100 => actuator fully open
  • value below 0 => fully closed
  • rest goes to dimmer
triggers:
  - id: "1"
    configuration:
      input: Gabinet_Virtual_Temperature
      setpoint: Gabinet_Desired_SetPoint
      kp: 65
      kd: 0
      kdTimeConstant: 0
      commandItem: Gabinet_PID_Control
      ki: 0.4
      loopTime: 60000
    type: pidcontroller.trigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      itemName: Gabinet_Desired_Dimmer
    type: core.ItemCommandAction

Now we need to have a piece of code to actually set up valve value.

/* Update Heater valve state based on value calculated by PID regulator */

var Room, Heater_Name;

Room = 'Gabinet';
Heater_Name = 'ZWaveNodeGabinet'


    /* ------------   Common (copy/paste)  ----------------- */

var DesiredDimmer_Name, ActualDimmer_Name;
var Desired_SetPoint_Name, Desired_SetPoint_Value;
var VirtualTemperature_Name, VirtualTemperature_Value;
var DesiredDimmerVal, DimmerNextVal, DimmerCurrentVal;

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

DesiredDimmer_Name = Room + '_Desired_Dimmer';
ActualDimmer_Name = Heater_Name + '_Dimmer';

Desired_SetPoint_Name = Room + '_Desired_SetPoint';
VirtualTemperature_Name = Room + '_Virtual_Temperature';

/* For debugging purposes */
VirtualTemperature_Value = itemRegistry.getItem(VirtualTemperature_Name).getState();
Desired_SetPoint_Value = itemRegistry.getItem(Desired_SetPoint_Name).getState();

logger.info(([Room, 'PID_Dimmer_To_Command', 'Items:', DesiredDimmer_Name, ActualDimmer_Name, Desired_SetPoint_Name, VirtualTemperature_Name].join(' | ')));
logger.info(([Room, ' | PID_Dimmer_To_Command | ', 'Desired SetPoint : ', Desired_SetPoint_Value, ' | ', 'Current Temp : ', VirtualTemperature_Value].join('')));

DesiredDimmerVal = itemRegistry.getItem(DesiredDimmer_Name).getState();
DimmerCurrentVal = itemRegistry.getItem(ActualDimmer_Name).getState();

/* PID can calculate values from minus infite to plus infinite */
/* Acutator works in 0 - 100% of valve state that means */
/* anything below 0 like -256 means valve closed => 0% */
/* anything above 100 like +156 means full valve open => 100%  */

if (DesiredDimmerVal != 'NULL') {
    if (DesiredDimmerVal > 0) {
        if (DesiredDimmerVal < 100) {
            DimmerNextVal = Math.round(DesiredDimmerVal);
        } else {
            DimmerNextVal = 100;
        }
    } else {
        DimmerNextVal = 0;
    }
} else {
    logger.Error('Desired Dimmer Value is not calculated properly');
	DimmerNextVal = 0;
}

logger.info(([Room, ' | PID_Dimmer_To_Command | ', 'Desired Dimmer: ', DesiredDimmerVal, ' | ', 'Calculated Dimmer: ', DimmerNextVal, ' | ', 'Current Dimmer: ', DimmerCurrentVal].join('')));

/* Update valve state with primitive battery saving */
/* Update Heater only on change and force it every 5 minutes */
/* Force update is for situations when heater was offline like (battery died or instalation procedure showed up */
/* DelayItem_value is a simple item representing integer updated every single minute (+1) and reset to 0 past 6 (minutes) */

var DelayItem_value, DelayItem_Name
DelayItem = 'DelayItem';
DelayItem_value = itemRegistry.getItem(DelayItem).getState();

if (DimmerCurrentVal != DimmerNextVal) {
    events.sendCommand(ActualDimmer_Name, DimmerNextVal);
} else if (DelayItem_value >= 5) {
    logger.info(([Room, ' | PID_Dimmer_To_Command | Forcing Dimmer update | DelayItem_value: ', DelayItem_value].join('')));
    events.sendCommand(ActualDimmer_Name, DimmerNextVal);
}

Run on every update of the Dimmer (PID execution)

Every time we need to change temperature we need to reset PID controller values.

triggers:
  - id: "1"
    configuration:
      itemName: Gabinet_Desired_SetPoint
    type: core.ItemStateUpdateTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      itemName: Gabinet_PID_Control
      command: RESET
    type: core.ItemCommandAction

Now this all would work without issues but we need to think also about user input that would like to manually change temperature that was set up by schedule. We need to rewrite the desired temperature to the one that was set up on heater then reset PID and then start to controll valve.

When user enters value Spirit is changing mode to 1 (so crappy algorithm kicks in). I will take that value once the heater will settle.

  1. Update Virtual Item desired setpoint,
  2. Change heater back to manufacture mode

So next rule would be:

var Room, Heater_Name

Room = 'Gabinet';
Heater_Name = 'ZWaveNodeGabinet'


/* ------------   Common (copy/paste)  ----------------- */

function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
}

var Desired_SetPoint_Name, PID_Control_Name, ActualSetPoint_Name, Heater_BasicCommand,Heater_Thermostatmode
var CurrentVirtualSetPoint, PhysicalSetPoint;

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

/* Sleep for a while so regulator could properly set up statuses */
java.lang.Thread.sleep(getRandomInt(3000, 5000));

/* Get the temperature that was set manually and rewrite it to be used by PID regulator */

Desired_SetPoint_Name = Room + '_Desired_SetPoint';
PID_Control_Name = Room + '_PID_Control'

ActualSetPoint_Name = Heater_Name + '_Setpointheat';
Heater_BasicCommand = Heater_Name + '_BasicDeprecated';
Heater_Thermostatmode = Heater_Name + '_Thermostatmode';

CurrentVirtualSetPoint = itemRegistry.getItem(Desired_SetPoint_Name).getState()
PhysicalSetPoint = itemRegistry.getItem(ActualSetPoint_Name).getState()

/* Don't allow to go below 18 °C */
if (PhysicalSetPoint < 18) {
    PhysicalSetPoint = 18
}
logger.info(([Room, ' | Chainging Virtual Setpoint |', 'Previous value: ', CurrentVirtualSetPoint, ' | Recieved value: ', PhysicalSetPoint].join('')));

/* Set up Desired set point to user preffered*/
events.sendCommand(Desired_SetPoint_Name, PhysicalSetPoint);

/* Sleep for a while then switch to manual dimmer mode */
java.lang.Thread.sleep(getRandomInt(800, 1000));
events.sendCommand(Heater_BasicCommand, 0xFE);
events.sendCommand(Heater_Thermostatmode, 31);

/* Reset PID regulator values after temp change*/
events.sendCommand(PID_Control_Name, 'RESET');

All room rules:

obraz

Helper utilities:

I have a simple battery saver option by forcing update every 5 min - I’m using a simple item incrementing value every 1 min.

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

var DelayItem_value

DelayItem_value = itemRegistry.getItem('DelayItem').getState();

logger.info(([' | DelayItem_value: ', DelayItem_value].join('')));

if (DelayItem_value <= 5) {
    events.sendCommand(String('DelayItem'), DelayItem_value + 1);
} else {
    events.sendCommand(String('DelayItem'), 1);
}

Synchronize heat values that was set is done every 20 min (in case some heaters would be down and missed the update timing) I’m processing all those at once in a loop (I left two things as a show up).

var ListOfThings, ThingsLen, SyncMode, i;

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

logger.info((['| Validate_Heater_Mode', '-------------------------'].join(' | ')));
logger.info((['| Validate_Heater_Mode', 'START'].join(' | ')));

ListOfThings = [
    { z_wave: 'ZWaveNodeSypialnia', virtual_item: 'Sypialnia' },
    { z_wave: 'ZWaveNodeGabinet', virtual_item: 'Gabinet' },
];


var Z_waveThingName, VirtualThingName

SyncMode = itemRegistry.getItem('HeaterSyncMode').getState();
//SyncMode = 'VIRTUAL_HEATER';
//SyncMode = 'USER';
logger.info((['| Validate_Heater_Mode | SyncMode: ', SyncMode].join('')));

ThingsLen = ListOfThings.length;
for (i = 0; i < ThingsLen; i++) {

    Z_waveThingName = ListOfThings[i].z_wave;
    VirtualThingName = ListOfThings[i].virtual_item;
    logger.info((['| Validate_Heater_Mode | Items: ', Z_waveThingName, ' : ', VirtualThingName].join('')));
    
    SynchroniseSetPointValues(SyncMode, Z_waveThingName, VirtualThingName);

    SetManufacturerMode(Z_waveThingName);

    SetHeaterMode(Z_waveThingName);
}

logger.info((['| Validate_Heater_Mode |', '-----------STOP--------------'].join(' | ')));

function SynchroniseSetPointValues(SyncMode, ZwaveName, DesiredName) {
 
    SynchroniseItems(SyncMode, ZwaveName, DesiredName);

    function SynchroniseItems(SyncMode, ZwaveName, DesiredName) {
        var Z_waveThing_SetPoint_Value;
        var Desired_SetPoint_Value;
    
        Z_waveThing_SetPoint_Value = GetSetPointValue(GetZwaveSetpointName(ZwaveName));
        Desired_SetPoint_Value = GetSetPointValue(GetDesiredSetPointName(DesiredName));

        logger.info((['| Validate_Heater_Mode | SetPoint: z_wave : ', Z_waveThing_SetPoint_Value, ' Desired: ', Desired_SetPoint_Value, ' |'].join('')));

        if (Z_waveThing_SetPoint_Value != Desired_SetPoint_Value) {
           
            if (SyncMode == 'VIRTUAL_HEATER') {
                SetItemSetPoint(GetZwaveSetpointName(ZwaveName), Desired_SetPoint_Value);
            } else if (SyncMode == 'USER') {
                SetItemSetPoint(GetDesiredSetPointName(DesiredName), Z_waveThing_SetPoint_Value);
            }
        }
        else
            logger.info((['| Validate_Heater_Mode | Nothing to do for SetPoint | '].join('')));
    }

    function GetSetPointValue(SetPointName) {
        var _SetPoint_Value
        _SetPoint_Value = itemRegistry.getItem(SetPointName).getStateAs(DecimalType.class).doubleValue();
        if (_SetPoint_Value == null) {
            _SetPoint_Value = DecimalType.ZERO;
        }
        return _SetPoint_Value
    }

    function GetDesiredSetPointName(DesiredName) {
        return DesiredName + '_Desired_SetPoint';
    }

    function GetZwaveSetpointName(ZwaveName) {
        return ZwaveName + '_Setpointheat';
    }

    function SetItemSetPoint(SetPointheatItemName, SetPoint_Value) {
        if ((SetPoint_Value != NULL) && (SetPoint_Value != 0)) {
            logger.info((['| Validate_Heater_Mode | Setting up SetPoint - z_wave = ', SetPoint_Value, ' |'].join('')));
            events.sendCommand(SetPointheatItemName, SetPoint_Value);
        } else
            logger.error((['| Validate_Heater_Mode | ', Desired_SetPoint_Name, ' | SetPoint is NULL |'].join('')));
    }
}


function SetManufacturerMode(Z_waveThingName) {

    Thermostatmode_BasicDeprecated = itemRegistry.getItem(Z_waveThingName + '_BasicDeprecated').getState();
    logger.info((['| Validate_Heater_Mode | ', Z_waveThingName, ' | BasicDeprecated: ', Thermostatmode_BasicDeprecated].join('')));
    if (Thermostatmode_BasicDeprecated != '254') {
        logger.info((['| Validate_Heater_Mode | ', Z_waveThingName, ' | Swtich to Manufactore Mode', ' | Basic', Thermostatmode_BasicDeprecated].join('')));
        events.sendCommand(Z_waveThingName + '_BasicDeprecated', 0xFE);
    } else
        logger.info((['| Validate_Heater_Mode | ', Z_waveThingName, ' | Nothing to do for BasicDeprecated  | '].join('')));
}

function SetHeaterMode(Z_waveThingName) {
    Thermostatmode = itemRegistry.getItem(Z_waveThingName + '_Thermostatmode').getState();
    logger.info((['| Validate_Heater_Mode | ', Z_waveThingName, ' | Thermostatmode: ', Thermostatmode].join('')));
    if (Thermostatmode == '1') {
        logger.info((['| Validate_Heater_Mode | ', Z_waveThingName, ' | Swtich to Manual Mode'].join('')));
        events.sendCommand(Z_waveThingName + '_Thermostatmode', 31);
    } else
        logger.info((['| Validate_Heater_Mode | ', Z_waveThingName, ' | Nothing to do for  Thermostatmode  | '].join('')));
}

I have a “feature” to override the heater input. Anything past 7:30 AM is controlled by user, anything past 19 is a VirtualHeater (cron)

triggers:
  - id: "1"
    configuration:
      time: 07:30
    type: timer.TimeOfDayTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      itemName: HeaterSyncMode
      command: USER
    type: core.ItemCommandAction

triggers:
  - id: "1"
    configuration:
      time: 19:05
    type: timer.TimeOfDayTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      itemName: HeaterSyncMode
      command: VIRTUAL_HEATER
    type: core.ItemCommandAction

Xiaomi was going offline quite frequently so I had to restart it

triggers:
  - id: "1"
    configuration:
      thingUID: mihome:gateway:d18d317234:7c49ebb19279
      status: OFFLINE
    type: core.ThingStatusChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/vnd.openhab.dsl.rule
      script: >
        logInfo("XiaomiBridgeOffline", "XiaomiBridge is offline!")

        var String result = executeCommandLine(Duration.ofSeconds(60),"/bin/sh", "-c", "$OPENHAB_RUNTIME/bin/client -p password bundle:restart org.openhab.binding.mihome")

        logInfo("result", result)

        logInfo("mihome:binding", "mihome:binding restarted")
    type: script.ScriptAction

Now finally.

It seems that Spirits are having sometimes problems to fully open and fully close. I created a simple rule to set up BOOST first to cause full open. Then I’m shutting those down (OFF) and finally I’m switching those ON.

obraz

BOOST

triggers:
  - id: "1"
    configuration:
      time: 20:15
    type: timer.TimeOfDayTrigger
conditions: []
actions:
  - inputs: {}
    id: "9"
    configuration:
      itemName: ZWaveNodeMalaLazienka_Thermostatmode
      command: "15"
    type: core.ItemCommandAction

OFF

triggers:
  - id: "1"
    configuration:
      time: 20:17
    type: timer.TimeOfDayTrigger
conditions: []
actions:
  - inputs: {}
    id: "9"
    configuration:
      itemName: ZWaveNodeMalaLazienka_Thermostatmode
      command: "0"
    type: core.ItemCommandAction

ON

triggers:
  - id: "1"
    configuration:
      time: 20:30
    type: timer.TimeOfDayTrigger
conditions: []
actions:
  - inputs: {}
    id: "9"
    configuration:
      itemName: ZWaveNodeMalaLazienka_Thermostatmode
      command: "1"
    type: core.ItemCommandAction

If you think about having such setup think twice as it was a constant battle. At the same time, I hope that this is a solid starting point so time is “wasted” on a much better solution at the end that I also could benefit :sweat_smile:

Setup summary:

  • 1x RaspberryPi 3B+ and OpenHabian 3.1
  • 1x Aeotec z-stick gen5
  • 2x NEO Coolcam Zwave Outlets
  • 8x Eurotronic Spirit
  • 1x Xiaomi Gateway
  • 9x Aquara temperature sensors

Possible upgrades:
I have a set of rules for every room - I don’t have time now but it would be best to rewrite it to process a list in a loop (like the last script for synchronization) and have 1 rule instead of set per room. This would significantly reduce complexity.

Disable heating control if avg temp at home is above X - like automatic summer break instead of rules by dates etc. So take all the sensors and calculate a value.

I still don’t have any proper UI for this solution so some templated would be helpfull.

PI4 was sometimes to slow to show Influx query on Grafana.

PS. Remember that if you will pick RaspberryPi 4 you have to pick Aeotec z-stick gen5 + (plus)

5 Likes

Reserved for updates.

I’m doing an experiment to solve the problem with devices switching to installation mode. My guess is that if the alkaline battery is not having enough juice to fully close the valve it’s causing this issue. I bought Li-ion 1.5V rechargeable batteries that provide constant voltage (underneath there is 3.7V aku and the regulator is switching it to 1.5V). I hope to have fewer issues.

The downside is that those are shutting down when voltage will drop below some value so there is no warning about the battery ending its life.

I bought random ones - Xtar AA R6 Li-ion 1.5V 2000mAh 3300mWh but those are not cheap and need special charger.

I found the same problem with the TRV not shutting fully at 0% and ‘leaking’ hot water through the radiator, especially when transitioning from almost closed to closed.

I used a similar method to always fully open the valve to 100% and then shut it to 0% whenever the PID controller indicated 0%. It seems the inertia of the motor spinning from 100% is enough to ensure a full shut off.

Here is my code snippet:

rule "Bedroom Central Heating Off"
when
    Item CentralHeatingState changed to OFF
then
    if ((BedroomTrvValvePosition.state as Number).intValue != 0)  {
        logInfo("Bedroom", "Closing Bedroom TRV valve")
        // exercise valve 100% to 0% as a workaround to maintain calibration and ensure shuts off fully)
        BedroomTrvValvePosition.sendCommand(100)
        createTimer(now.plusSeconds(30), [| BedroomTrvValvePosition.sendCommand(0) ])
        }
end

I have very different experience with the valves. Mine work very well. The only control I send is a move from economy to heat when the room is going to be occupied. The PID in the valves works well with very little overshoot and battery life is greater than 1 year with some now heading for 18 months on original batteries.

Is it possible you have some faulty ones or issues in your zwave network?

I doubt I have problems with network as I bought the NEO Coolcam Zwave Outlets to extend the signal. I don’t have errors about missing devices.

It’s a question what firmware you have internally (I think I saw v15/v16 in my batch) or if there were any hardware upgrades over time. Maybe they fixed something between batches but they don’t offer any firmware upgrade - I bought mine in 2018.

If you will search for threads regarding Eurotronic Spirit devices more people had problems with those and searched for option to control it manually.

Now definetly my external control is draining more battery as device has to wake up from deep sleep instead just control valve position.

I also was thinking if the valve type in the heater might be causing issues. If I remove Spirit the pin moves freely but I need to use quite a lot of force to push it to close the valve. Could that cause battery drain I don’t know.

I used Varta Industrial pro and those lasted only 1 month before I started to have problems with device going in to installation mode. When I tried to use Eneloop I had Err1 even during installation.

No problems with Li-ion in other rooms. I’ll buy more of those once China will send those to my country as those are unavailable any more.

It stinks when vendors make bad products, fix them but do not make fix available to early customers.

Hi @Dominik_Jeziorski ,

Thanks for a good PID concept and for taking the time to discuss the important temperature regimes.

I have 12 Eurotronic Spirit+ actuators (no upgrades) all operating by setting their mode and would offer the following

  • After 2 years of problems with z-wave failures I also realised that you had to have a spine of powered (always on) units to maintain the network.

  • I use rechargeable batteries in all units but change at 20% - the voltage drop after this looks similar to a flat alkaline battery and caused the “Inclusion” error

  • Valves taking over a minute to fully cycle means there is an actuator/valve mismatch, mine go from fully open to closed in about 15 seconds. Valves not moving typically are due to two possible problems

  • “Wind-up” - the PID calculation calculates an open/close position greater than 100/0% and it takes a long time for the algorithm to output a value between 0 & 100%. I see you have anti-wind-up control in your software.

  • Wrong valve - the actuator simply cannot move the valve (stiff valve &/or flow in the wrong direction); domestically I suspect wrong flow to be a common problem - many valves typically only allow flow in one direction and if the actuator is fighting the flow it usually fails.

Hope this helps

I wonder if you are using any particular type of rechargeable batteries that is working for you. In my case I tried Ikea Lada, Eneloop and Everactive - all ended up having INT errors after some time. My feeling is that it’s usually happening when valve have to be fully closed and higher amperage is needed. Why I think so - this issue happened more frequently in my bedroom where valve was more frequently closed compared to the bathroom where it would be usually opened even at 10% .

I switched to batteries and those lasted longer but I’m not sure if last batch that I bought (like pack of 40 pieces) is worse (aged) or what?

I read somewhere that updates in the original software was updating valve every 2 min. I’ll probably need to look to Grafana to check if there is any optimization I could do. Like update valve position less frequently (every 1 min now - but event is sent only if needed).

I think opening/closing is within 15s you mentioned. I waited 2 min before sending Off because I just saw sometimes delay in valve actual action.

Boost/Off/On is a simple approach. The majority of valves should be closing in my case around 20:00 (Temp set to 19C while temp in the room is usually 22C at that time). So shutting those down was on purpose to decrease temp if it would be still high.PID should pick it up and set valve to 0.

Morning @Dominik_Jeziorski,

Batteries are Amazon’s basic - cheap.
I’ve never found updates to the actuator firmware (currently 0.16).
The INT error is quite puzzling - I also shut the valves off overnight without issue.

On battery life you could try relating the update period (polling) to the temperature difference:
For example, one room takes 100 minutes to change from 15°C to 21°C so I would have a very low polling rate which increases as the setpoint is achieved.

I rely on over the air updates when it goes to temperature so there is not much pulling from devices in that aspect.

Once any device (Xiaomi or Spirit) will update temperature is then pushed to virtual temp sensor and PID rely on it as kind of static source of data.

In other direction pushing the updates - PID provides value for valve. If it’s the same as previous I’m not pushing it. Then force push is every 5 min “just in case”.

The only optimizations I could think of is to increse PID loop to 2 min and maybe do force push only in case I would somehow detect that node was gone.