Integrating Idegis Domotic 2 LS Pool Controller with OpenHAB via Modbus

Overview

This tutorial provides a step-by-step guide to integrating the Idegis Domotic 2 LS pool controller with OpenHAB using the Modbus binding. By following this tutorial, you’ll be able to:

  • Monitor your pool’s key parameters (pH, temperature, salt level)
  • Control the pool pump through OpenHAB
  • Monitor and control the salt electrolysis system
  • Interpret status messages and alarms
  • Build advanced automation based on pool data

The integration uses Modbus over a serial connection, allowing direct access to the controller’s internal registers.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Hardware Connection
  4. Understanding the Modbus Interface
  5. Pool Pump Control via Output1
  6. Implementation Overview
  7. Things Configuration
  8. JavaScript Transformations
  9. Items Configuration
  10. Rules
  11. Advanced Usage and Applications
  12. Modbus Register Bit Access in OpenHAB
  13. Sensor Validation Logic
  14. Troubleshooting
  15. Conclusion

Introduction

This tutorial explains how to integrate an Idegis Domotic 2 LS pool controller with OpenHAB using the Modbus binding. The Idegis Domotic 2 LS is a comprehensive pool control system that manages chlorine production through electrolysis, pH measurement, temperature monitoring, and more.

I’ve been working with the manufacturer-provided Modbus register documentation, which was very helpful. While the register documentation was provided by the manufacturer, interpreting the values and translating the status bits into user-friendly information took some time and effort.

Prerequisites

Before starting, ensure you have:

  • A working OpenHAB installation (tested with OpenHAB 4.3)
  • The following OpenHAB add-ons installed:
    • Modbus binding
    • JavaScript transformation
  • A working serial connection to the pool controller (RS485 to USB adapter)
  • Basic understanding of OpenHAB Things, Items, and Rules

Hardware Connection

The Idegis Domotic 2 LS uses RS485 for Modbus communication. You’ll need a suitable RS485 to USB adapter to connect it to your OpenHAB server. In my setup, I’m using a dedicated adapter that I’ve given a custom name in my system.

Important note: Make sure your serial interface is properly configured in your system. This tutorial assumes you already have a working serial interface. For general information on setting up serial interfaces in OpenHAB, refer to the community documentation.

Understanding the Modbus Interface

The Idegis Domotic 2 LS supports the following Modbus functions:

  • Reading Input Registers (Function 0x04)
  • Reading Holding Registers (Function 0x03)
  • Writing Single Registers (Function 0x06)
  • Writing Multiple Registers (Function 0x10)

The default serial parameters are:

  • Baud rate: 9600
  • Data bits: 8
  • Parity: even
  • Stop bits: 1
  • Default slave ID: 1 (configurable at register 0x00)

Note: In my configuration, I’ve modified some of the Modbus addresses to suit my specific setup. You might need to adjust these according to your controller’s configuration.

Pool Pump Control via Output1

In most standard installations, the pool pump is connected to Output1 of the controller. To control this output, bits 14 and 15 (Segment 4) of the output register must be modified. The rules in this tutorial provide a way to turn the pump on and off by manipulating these bits. In my home setup, I control the pump completely based on my PV system output, so I don’t use the controller’s internal timer functionality.

Implementation Overview

The implementation consists of three main parts:

  1. Things configuration: Defines the Modbus connection and registers to poll
  2. Items configuration: Maps the Modbus registers to OpenHAB items
  3. Rules: Processes the raw Modbus data and implements advanced logic

Things Configuration

Let’s start with the Things configuration. This sets up the Modbus polling and defines what registers to read.

// Idegis Domotic 2 Pool Controller - Modbus Configuration

Bridge modbus:serial:poolController "Idegis Domotic 2 Pool Controller" [
    port="/dev/ttyModBus",  // Change this to match your serial port
    baud=9600,
    dataBits=8,
    parity="even",
    stopBits="1.0",
    encoding="rtu",
    timeBetweenTransactionsMillis=35,
    connectMaxTries=1,
    connectTimeoutMillis=10000,
    receiveTimeoutMillis=1500,
    flowControlIn="none",
    flowControlOut="none",
    echo=false,
    id=1
] {
    // Device information poller - polls basic device information and capabilities
    Bridge poller deviceInfoPoller "Idegis Domotic 2 Device Info Poller" [
        start=0,
        length=7,
        refresh=300000, // Every 5 minutes, as these values rarely change
        maxTries=3,
        cacheMillis=50,
        type="holding"
    ] {
        Thing data deviceAddress "Idegis Domotic 2 Modbus Device Address" [
            readStart="0",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data implementedTechnologies "Idegis Domotic 2 Implemented Technologies" [
            readStart="6",
            readValueType="uint16",
            readTransform="default"
        ]
    }
    
    // Time register poller
    Bridge poller timePoller "Idegis Domotic 2 Time Register Poller" [
        start=240,  // 0xF0
        length=3,   // 3 registers: 0xF0, 0xF1, 0xF2
        refresh=60000, // Every minute
        maxTries=3,
        cacheMillis=50,
        type="holding"
    ] {
        Thing data yearMonth "Idegis Domotic 2 Year/Month" [
            readStart="240",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data dayHour "Idegis Domotic 2 Day/Hour" [
            readStart="241",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data minuteSecond "Idegis Domotic 2 Minute/Second" [
            readStart="242",
            readValueType="uint16",
            readTransform="default"
        ]
    }

    // Status and alarm poller
    Bridge poller statusPoller "Idegis Domotic 2 Status and Alarm Poller" [
        start=32,
        length=10,
        refresh=30000, // Every 30 seconds for alarms and status
        maxTries=3,
        cacheMillis=50,
        type="input"
    ] {
        Thing data generalStatus "Idegis Domotic 2 General Status" [
            readStart="32",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data flowAlarms "Idegis Domotic 2 Flow Alarms" [
            readStart="36",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data electrolysisAlarms "Idegis Domotic 2 Electrolysis Alarms" [
            readStart="37",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data phAlarms "Idegis Domotic 2 PH Alarms" [
            readStart="38",
            readValueType="uint16",
            readTransform="default"
        ]
    }
    
    // Electrolysis poller
    Bridge poller electrolysisPoller "Idegis Domotic 2 Electrolysis Poller" [
        start=64,
        length=14,  // From register 0x40 to 0x4D (14 registers)
        refresh=60000, // Every 60 seconds
        maxTries=3,
        cacheMillis=50,
        type="input"
    ] {
        Thing data electrolysisStatus "Idegis Domotic 2 Electrolysis Status" [
            readStart="64",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data productionPctTarget "Idegis Domotic 2 Electrolysis Production Target" [
            readStart="65",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data productionPctNow "Idegis Domotic 2 Electrolysis Current Production Percentage" [
            readStart="66",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data currentElectrodes "Idegis Domotic 2 Electrolysis Current Output Current" [
            readStart="67",
            readValueType="uint16",
            readTransform="JS(divide100.js)"
        ]
        
        Thing data voltageElectrodes "Idegis Domotic 2 Electrolysis Current Output Voltage" [
            readStart="68",
            readValueType="uint16",
            readTransform="JS(divide100.js)"
        ]
        
        Thing data gHourProductionNow "Idegis Domotic 2 Electrolysis Current Chlorine Production" [
            readStart="69",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data gProductionToday "Idegis Domotic 2 Electrolysis Chlorine Production Today" [
            readStart="70",
            readValueType="uint16",
            readTransform="JS(divide1000.js)"
        ]
        
        Thing data timeElectrolysisToday "Idegis Domotic 2 Electrolysis Runtime Today" [
            readStart="71",
            readValueType="uint16",
            readTransform="default"
        ]
        
        // Use 32-bit reading for operating hours
        Thing data hoursRunningElectTotal "Idegis Domotic 2 Electrolysis Total Operating Hours" [
            readStart="72",
            readValueType="uint32",
            readTransform="default"
        ]
        
        Thing data hoursRunningElectPartial "Idegis Domotic 2 Electrolysis Partial Operating Hours" [
            readStart="74",
            readValueType="uint32",
            readTransform="default"
        ]
        
        Thing data totalResetElect "Idegis Domotic 2 Electrolysis Number of Resets" [
            readStart="76",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data gProductionThisHour "Idegis Domotic 2 Electrolysis Chlorine Production This Hour" [
            readStart="77",
            readValueType="uint16",
            readTransform="JS(divide1000.js)"
        ]
    }

    // Measurements poller (pH, temperature, etc.)
    Bridge poller measurementsPoller "Idegis Domotic 2 Measurements Poller" [
        start=81,
        length=100,
        refresh=60000, // Every 60 seconds for measurements
        maxTries=3,
        cacheMillis=50,
        type="input"
    ] {
        // === PH VALUES ===
        Thing data phValue "Idegis Domotic 2 PH Value" [
            readStart="81",
            readValueType="uint16",
            readTransform="JS(divide100.js)"
        ]
        
        Thing data phValueStdCal "Idegis Domotic 2 PH Value (Standard Calibration)" [
            readStart="82",
            readValueType="uint16",
            readTransform="JS(divide100.js)"
        ]
        
        Thing data phStatusOutput "Idegis Domotic 2 PH Status Output" [
            readStart="86",
            readValueType="uint16",
            readTransform="default"
        ]
        
        // === TEMPERATURE ===
        Thing data poolTemperature "Idegis Domotic 2 Pool Temperature" [
            readStart="177",
            readValueType="uint16",
            readTransform="JS(divide10.js)"
        ]
        
        Thing data poolTempStdCal "Idegis Domotic 2 Pool Temperature (Standard Calibration)" [
            readStart="178",
            readValueType="uint16",
            readTransform="JS(divide10.js)"
        ]
    }
    
    // Salt status poller
    Bridge poller saltPoller "Idegis Domotic 2 Salt Level Poller" [
        start=192,
        length=3,
        refresh=60000,
        maxTries=3,
        cacheMillis=50,
        type="input"
    ] {
        Thing data saltStatus "Idegis Domotic 2 Salt Status" [
            readStart="192",
            readValueType="uint16",
            readTransform="default"
        ]
        
        Thing data saltValue "Idegis Domotic 2 Salt Level" [
            readStart="193",
            readValueType="uint16",
            readTransform="JS(divide100.js)"
        ]
    }
    
    // Control poller (for outputs)
    Bridge poller controlPoller "Idegis Domotic 2 Control Register Poller" [
        start=272,
        length=1,
        refresh=60000,
        maxTries=3,
        cacheMillis=50,
        type="holding"
    ] {
        Thing data output1Control "Idegis Domotic 2 Output 1 Control" [
            readStart="272",
            readValueType="uint16",
            readTransform="default",
            writeValueType="int16",
            writeStart="272",
            writeTransform="default",
            writeType="holding"
        ]
    }
}

JavaScript Transformations

Before continuing with the Items, we need to set up the JavaScript transformations used to convert the raw Modbus values. Create the following files in your OpenHAB transform directory:

divide10.js

(function(i) {
    return parseFloat(i) / 10.0;
})(input)

divide100.js

(function(i) {
    return parseFloat(i) / 100.0;
})(input)

divide1000.js

(function(i) {
    return parseFloat(i) / 1000.0;
})(input)

Items Configuration

Now let’s define the Items to store and display the Modbus data:

// === MAIN GROUPS ===
Group gPool                  "Pool"                      <water>     
Group gPool_Status           "Pool Status"               <status>    (gPool)
Group gPool_Measurements     "Measurements"              <chart>     (gPool) 
Group gPool_Control          "Control"                   <switch>    (gPool)
Group gPool_Alarms           "Alarms"                    <siren>     (gPool)
Group gPool_Technical        "Technical Details"         <settings>  (gPool)
Group gPool_Electrolysis     "Electrolysis Details"      <energy>    (gPool_Technical)

// === GROUP FOR RAW DATA (not displayed in UI) ===
Group gPool_RawData "Pool Raw Data" // Not a member of gPool!

// === DEVICE INFORMATION ===
Number Pool_Device_Address "Modbus Device Address [%d]" <network> (gPool_Technical) { channel="modbus:data:poolController:deviceInfoPoller:deviceAddress:number" }
Number Pool_Implemented_Technologies "Implemented Technologies [%d]" <settings> (gPool_Technical) { channel="modbus:data:poolController:deviceInfoPoller:implementedTechnologies:number" }

// === MEASUREMENTS (ONLY AVAILABLE SENSOR VALUES) ===
Number:Temperature Pool_Temperature "Pool Temperature [%.1f °C]" <temperature> (gPool_Measurements)
Number:Dimensionless Pool_PH "PH Value [%.2f]" <water> (gPool_Measurements)
Number:Dimensionless Pool_Salinity "Salt Level [%.2f ppt]" <water> (gPool_Measurements)

// === QUALITATIVE EVALUATIONS ===
String Pool_PH_Quality "PH Quality [%s]" <water> (gPool_Measurements)
String Pool_Temperature_Status "Temperature Status [%s]" <temperature> (gPool_Measurements)
String Pool_Salinity_Status "Salt Level Status [%s]" <water> (gPool_Measurements)

// === RAW DATA: TEMPERATURE ===
Number:Temperature Pool_Temperature_Raw "Pool Temperature (Raw) [%.1f °C]" <temperature> (gPool_RawData) { channel="modbus:data:poolController:measurementsPoller:poolTemperature:number" }
Number:Temperature Pool_Temperature_StdCal "Pool Temperature (Standard Calibration) [%.1f °C]" <temperature> (gPool_Technical) { channel="modbus:data:poolController:measurementsPoller:poolTempStdCal:number" }

// === RAW DATA: PH VALUES ===
Number:Dimensionless Pool_PH_Raw "PH Value (Raw) [%.2f]" <water> (gPool_RawData) { channel="modbus:data:poolController:measurementsPoller:phValue:number" }
Number:Dimensionless Pool_PH_StdCal "PH Value (Standard Calibration) [%.2f]" <water> (gPool_Technical) { channel="modbus:data:poolController:measurementsPoller:phValueStdCal:number" }
Number Pool_PH_Status "PH Status Output [%d]" <status> (gPool_Technical) { channel="modbus:data:poolController:measurementsPoller:phStatusOutput:number" }

// === RAW DATA: SALT LEVEL ===
Number:Dimensionless Pool_Salinity_Raw "Salt Level (Raw) [%.2f ppt]" <water> (gPool_Measurements) { channel="modbus:data:poolController:saltPoller:saltValue:number" }
// === SALT STATUS ===
Number Pool_Salt_Status "Salt Status Value [%d]" <water> (gPool_Technical) { channel="modbus:data:poolController:saltPoller:saltStatus:number" }
String Pool_Salt_Statustext "Salt Status [%d]" <water> (gPool_Technical)

// Derived items for individual bits of salt status
Switch Pool_Salt_Current_Too_Low "Current too low for salt measurement [%s]" <e> (gPool_Alarms)
Switch Pool_Salt_Measure_Unreliable "Salt measurement unreliable [%s]" <e> (gPool_Alarms)
Switch Pool_Salt_Voltage_Too_Low "Voltage too low for salt measurement [%s]" <e> (gPool_Alarms)
Switch Pool_Salt_Calibration_OK "Salt calibration OK [%s]" <check> (gPool_Status)

// === STATUS AND ALARMS ===
Number Pool_General_Status "General Status [%d]" <status> (gPool_Technical) { channel="modbus:data:poolController:statusPoller:generalStatus:number" }
Number Pool_Flow_Alarms "Flow Alarms [%d]" <siren> (gPool_Technical) { channel="modbus:data:poolController:statusPoller:flowAlarms:number" }
Number Pool_Electrolysis_Alarms "Electrolysis Alarms [%d]" <siren> (gPool_Technical) { channel="modbus:data:poolController:statusPoller:electrolysisAlarms:number" }
Number Pool_PH_Alarms "PH Alarms [%d]" <siren> (gPool_Technical) { channel="modbus:data:poolController:statusPoller:phAlarms:number" }

// === ELECTROLYSIS ITEMS ===
Number Pool_Electrolysis_Status "Electrolysis Status [%d]" <status> (gPool_Technical) { channel="modbus:data:poolController:electrolysisPoller:electrolysisStatus:number" }

// === DETAILED ELECTROLYSIS ITEMS ===
Number Pool_Electrolysis_ProductionTarget "Electrolysis Target [%d %%]" <settings> (gPool_Electrolysis) { channel="modbus:data:poolController:electrolysisPoller:productionPctTarget:number" }
Number Pool_Electrolysis_ProductionCurrent "Current Production [%d %%]" <energy> (gPool_Electrolysis) { channel="modbus:data:poolController:electrolysisPoller:productionPctNow:number" }
Number:ElectricCurrent Pool_Electrolysis_Current "Electrode Current [%.2f A]" <energy> (gPool_Electrolysis) { channel="modbus:data:poolController:electrolysisPoller:currentElectrodes:number" }
Number:ElectricPotential Pool_Electrolysis_Voltage "Electrode Voltage [%.2f V]" <energy> (gPool_Electrolysis) { channel="modbus:data:poolController:electrolysisPoller:voltageElectrodes:number" }
Number:Mass Pool_Electrolysis_ChlorinePerHour "Chlorine Production [%d g/h]" <flow> (gPool_Electrolysis) { channel="modbus:data:poolController:electrolysisPoller:gHourProductionNow:number" }
Number:Mass Pool_Electrolysis_ChlorineToday "Chlorine Produced Today [%d g]" <flow> (gPool_Electrolysis) { channel="modbus:data:poolController:electrolysisPoller:gProductionToday:number" }
Number Pool_Electrolysis_RuntimeToday "Runtime Today [%d min]" <time> (gPool_Electrolysis) { channel="modbus:data:poolController:electrolysisPoller:timeElectrolysisToday:number" }

// 32-bit values read directly
Number Pool_Electrolysis_TotalHours "Total Runtime [%d h]" <time> (gPool_Electrolysis) { channel="modbus:data:poolController:electrolysisPoller:hoursRunningElectTotal:number" }
Number Pool_Electrolysis_PartialHours "Partial Runtime [%d h]" <time> (gPool_Electrolysis) { channel="modbus:data:poolController:electrolysisPoller:hoursRunningElectPartial:number" }

Number Pool_Electrolysis_ResetCounter "Reset Counter [%d]" <counter> (gPool_Electrolysis) { channel="modbus:data:poolController:electrolysisPoller:totalResetElect:number" }
Number:Mass Pool_Electrolysis_ChlorineThisHour "Chlorine This Hour [%d g]" <flow> (gPool_Electrolysis) { channel="modbus:data:poolController:electrolysisPoller:gProductionThisHour:number" }

// === CONTROL ===
Number Pool_Output1_Control "Output 1 Control [%d]" <switch> (gPool_Technical) { channel="modbus:data:poolController:controlPoller:output1Control:number" }
Switch Pool_Output1_An "Pool Pump" <pump> (gPool_Control)
Switch Pool_Output1_InterneAutomatik "Automatic Control" <switch> (gPool_Control)

// === DEFAULT VALUES FOR RULES AND AUTOMATION ===
Switch Pool_Filtration_Needed "Filtration Needed [%s]" <status> (gPool_Control, gPersistStartup)
Switch Pool_Lock "Pool Control Locked [%s]" <lock> (gPool_Control, gPersistStartup)
Switch Pool_Auto_Lock "Auto-Lock Active [%s]" <lock> (gPool_Control, gPersistStartup)
Number Pool_Pump_Runtime_Today "Runtime Today [%d s]" <time> (gPool_Status, gPersistStartup)
Number Pool_Pump_Start "Start Time [%d]" <clock> (gPool_Technical, gPersistStartup)

// === DERIVED STATUS ITEMS (updated by rules) ===
Switch Pool_Alarm "Pool Alarm [%s]" <siren> (gPool_Alarms)
Switch Pool_Flow_Error "Flow Error [%s]" <flow> (gPool_Alarms)
Switch Pool_Electrolysis_Running "Electrolysis Active [%s]" <switch> (gPool_Status)
String Pool_Status "Pool Status [%s]" <status> (gPool_Status)
Number Pool_Pump_Runtime "Pump Runtime Since Start [%d s]" <time> (gPool_Status)

// === DETAILED STATUS ITEMS (populated by rules) ===
Switch Pool_Has_General_Alarm "General Alarm [%s]" <siren> (gPool_Alarms)
Switch Pool_Treatment_Halted "Treatment Halted [%s]" <status> (gPool_Status)
Switch Pool_Low_PH_Alarm "PH Too Low [%s]" <water> (gPool_Alarms)
Switch Pool_High_PH_Alarm "PH Too High [%s]" <water> (gPool_Alarms)
Switch Pool_Low_Conductivity "Conductivity Too Low [%s]" <water> (gPool_Alarms)
Switch Pool_High_Conductivity "Conductivity Too High [%s]" <water> (gPool_Alarms)

// === SENSOR STATUS ===
Switch Pool_Sensors_Valid "Sensors Providing Valid Values" <status> (gPool_Status)

// Time register items
Number Pool_System_YearMonth "System Year/Month [%d]" <time> (gPool_RawData) { channel="modbus:data:poolController:timePoller:yearMonth:number" }
Number Pool_System_DayHour "System Day/Hour [%d]" <time> (gPool_RawData) { channel="modbus:data:poolController:timePoller:dayHour:number" }
Number Pool_System_MinuteSecond "System Minute/Second [%d]" <time> (gPool_RawData) { channel="modbus:data:poolController:timePoller:minuteSecond:number" }

// Derived items for formatted time
String Pool_System_Date "System Date [%s]" <calendar> (gPool_Technical)
String Pool_System_Time "System Time [%s]" <time> (gPool_Technical)
String Pool_System_DateTime "System Date/Time [%s]" <calendar> (gPool_Technical)

Rules

Let’s implement the rules that process the Modbus data and implement advanced logic. I’ll provide several key rule files:

1. Pool_Alarmauswertung.rules - Alarm Processing

This rule file processes alarms and status information from the controller:

// Idegis Domotic 2 Pool - Rules for alarm evaluation
// ===============================================================

// Status and alarm processing for the pool controller
rule "Pool Status and Alarm Processing"
when
    Member of gPool_Technical received update
then
    // Process general status
    if (Pool_General_Status.state != NULL) {
        val statusValue = (Pool_General_Status.state as Number).intValue
        
        // Bit 0 (0x01): General alarm - Check if odd number
        val hasAlarm = statusValue % 2 == 1
        Pool_Alarm.postUpdate(if(hasAlarm) ON else OFF)
        
        // Bit 2 (0x04): Treatment halted - Check with division
        val treatmentHalted = (statusValue / 4) % 2 == 1
        
        // Update status text
        var String statusText = "Normal"
        if (hasAlarm) statusText = "Alarm"
        if (treatmentHalted) statusText = "Treatment halted"
        
        Pool_Status.postUpdate(statusText)
    }
    
    // Process flow alarms
    if (Pool_Flow_Alarms.state != NULL) {
        val flowAlarmsValue = (Pool_Flow_Alarms.state as Number).intValue
        val hasFlowError = flowAlarmsValue > 0
        Pool_Flow_Error.postUpdate(if(hasFlowError) ON else OFF)
    }
    
    // Process electrolysis status
    if (Pool_Electrolysis_Status.state != NULL) {
        val electroValue = (Pool_Electrolysis_Status.state as Number).intValue
        
        // Bit 0 (0x01): Electrolysis running - Check if odd number
        val electroRunning = electroValue % 2 == 1
        Pool_Electrolysis_Running.postUpdate(if(electroRunning) ON else OFF)
    }
end

// Detailed alarm evaluation for PH values
rule "PH Alarm Evaluation"
when
    Item Pool_PH_Alarms received update
then
    if (Pool_PH_Alarms.state != NULL) {
        val phAlarmValue = (Pool_PH_Alarms.state as Number).intValue
        
        // Bit 0 (0x01): PH too low - Check if odd number
        val lowPH = phAlarmValue % 2 == 1
        Pool_Low_PH_Alarm.postUpdate(if(lowPH) ON else OFF)
        
        // Bit 1 (0x02): PH too high - Check with division and modulo
        val highPH = (phAlarmValue / 2) % 2 == 1
        Pool_High_PH_Alarm.postUpdate(if(highPH) ON else OFF)
        
        if (lowPH || highPH) {
            logWarn("Pool", "PH value alarm: " + (if(lowPH) "Too low" else "") + (if(highPH) "Too high" else ""))
        }
    }
end

// Electrolysis alarm evaluation for Low-Salt system
rule "Electrolysis Alarm Evaluation"
when
    Item Pool_Electrolysis_Alarms received update
then
    if (Pool_Electrolysis_Alarms.state != NULL) {
        val electroAlarmValue = (Pool_Electrolysis_Alarms.state as Number).intValue
        
        // Bit 1 (0x02): Conductivity too low (important for Low-Salt) - Check with division and modulo
        val lowConductivity = (electroAlarmValue / 2) % 2 == 1
        Pool_Low_Conductivity.postUpdate(if(lowConductivity) ON else OFF)
        
        // Bit 2 (0x04): Conductivity too high - Check with division and modulo
        val highConductivity = (electroAlarmValue / 4) % 2 == 1
        Pool_High_Conductivity.postUpdate(if(highConductivity) ON else OFF)
        
        if (lowConductivity) {
            logWarn("Pool", "Low-Salt system: Conductivity too low - Check salt level")
        } else if (highConductivity) {
            logWarn("Pool", "Low-Salt system: Conductivity too high - Check salt level")
        }
    }
end

2. Pool_messwerte.rules - Sensor Value Management

This rule file handles sensor value processing, validation, and quality assessment:

// Idegis Domotic 2 Pool - Rules for sensor value management
// ===============================================================

// ====== CONFIGURABLE THRESHOLDS ======
// These values can be easily adjusted without changing the rules themselves

// PH value thresholds
val Number PH_MIN = 7.0          // Lower limit for optimal PH value
val Number PH_MAX = 7.6          // Upper limit for optimal PH value

// Temperature thresholds
val Number TEMP_MIN = 24.0       // Lower limit for comfortable temperature
val Number TEMP_MAX = 30.0       // Upper limit for comfortable temperature

// Salt level thresholds (Low-Salt system)
val Number SALT_MIN = 1.0        // Absolute lower limit for salt level
val Number SALT_MAX = 3.0        // Absolute upper limit for salt level
val Number SALT_OPTIMAL_MIN = 1.8 // Lower limit for optimal salt level
val Number SALT_OPTIMAL_MAX = 2.2 // Upper limit for optimal salt level
val Number SALT_IDEAL = 2.0      // Ideal salt level according to manual

// Timer for pump runtime check
var Timer sensorValidityTimer = null
val MIN_RUNTIME_FOR_VALID_SENSORS = 180 // 3 minutes

// ====== RULES ======

rule "Pool Measurement Update"
/**
* Rule for updating pool measurements
* 
* Operation:
* - Raw data is read directly from the Modbus interface (Pool_*_Raw items)
* - Updates occur only when sensors are marked as valid
* - All raw data is transferred synchronously to the display items
* 
* Background:
* - The RAW items are continuously read from Modbus
* - Sensor validity depends on pool pump operation
* - Separate display items are only updated when sensors are valid
* 
* Related rule: Monitoring pump start to validate sensors (Pool Pump Status Monitoring)
*/
when
    Item Pool_Temperature_Raw changed or
    Item Pool_PH_Raw changed or
    Item Pool_Salinity_Raw changed
then
    // Check if sensors are valid
    if (Pool_Sensors_Valid.state == ON) {
        // Update all three values
        Pool_Temperature.postUpdate(Pool_Temperature_Raw.state)
        Pool_PH.postUpdate(Pool_PH_Raw.state)
        Pool_Salinity.postUpdate(Pool_Salinity_Raw.state)
        
        logInfo("PoolSensors", "All pool values updated")
    } else {
        logInfo("PoolSensors", "Sensors not valid - no update")
    }
end

rule "Pool Pump Status Monitoring"
/**
* Rule for monitoring and validating pool sensors based on pump status
* 
* Operation:
* - Monitors switching the pool pump on and off
* - Implements a waiting period of 3 minutes before sensors are considered valid
* - Ensures sensor measurements are only considered reliable when the pump is running
* 
* Behavior when turning on the pump:
* - Sensors are initially marked as invalid
* - 3-minute timer is started
* - After 3 minutes, sensors are marked as valid
* - Current measurements are transferred to display items
* 
* Behavior when turning off the pump:
* - Sensors are immediately marked as invalid
* - Display items are reset
* - Running validation timer is cancelled
* 
* Background:
* - Sensor values are only reliable when the pump is running
* - Short startup time is considered to obtain stable measurements
* - Clear separation between raw data and validated display values
*/
when
    Item Pool_Output1_An changed
then
    if (Pool_Output1_An.state == ON) {
        // Pump was turned on
        Pool_Sensors_Valid.postUpdate(OFF) // Mark sensors as invalid initially
        
        // Start timer for checking sensor values
        if (sensorValidityTimer !== null) {
            sensorValidityTimer.cancel()
        }
        
        sensorValidityTimer = createTimer(now.plusSeconds(MIN_RUNTIME_FOR_VALID_SENSORS), [ |
            // After 3 minutes: Mark sensors as valid
            Pool_Sensors_Valid.postUpdate(ON)
            logInfo("PoolSensors", "Pump has been running for 3 minutes, sensor values are considered valid")
            
            // Transfer current measurements to display items
            if (Pool_Temperature_Raw.state !== NULL) {
                Pool_Temperature.postUpdate(Pool_Temperature_Raw.state)
            }
            if (Pool_PH_Raw.state !== NULL) {
                Pool_PH.postUpdate(Pool_PH_Raw.state)
            }
            if (Pool_Salinity_Raw.state !== NULL) {
                Pool_Salinity.postUpdate(Pool_Salinity_Raw.state)
            }
            
            sensorValidityTimer = null
        ])
    } else {
        // Pump was turned off
        Pool_Sensors_Valid.postUpdate(OFF)
        
        // Reset all display items to make it clear no valid values are available
        Pool_Temperature.postUpdate(NULL)
        Pool_PH.postUpdate(NULL)
        Pool_Salinity.postUpdate(NULL)
        
        // Cancel timer if it's still running
        if (sensorValidityTimer !== null) {
            sensorValidityTimer.cancel()
            sensorValidityTimer = null
        }
        
        logInfo("PoolSensors", "Pump was turned off, sensor values are no longer valid")
    }
end

/**
 * Rule: Alarm evaluation only with valid sensor values
 * Evaluates alarms only when the pump has been running for at least 3 minutes
 */
rule "Pool Alarm Evaluation"
when
    Item Pool_Flow_Alarms changed or
    Item Pool_Electrolysis_Alarms changed or
    Item Pool_PH_Alarms changed or
    Item Pool_General_Status changed
then
    // Only evaluate alarms when sensors are valid
    if (Pool_Sensors_Valid.state == ON) {
        // Process general status (Register 0x20)
        if (triggeringItem == Pool_General_Status && Pool_General_Status.state !== NULL) {
            // Simplified bit evaluation with modulo division instead of bit operations
            var statusValue = (Pool_General_Status.state as DecimalType).intValue
            
            // Bit 0 (0x01): General alarm - Check if odd number
            var hasAlarm = statusValue % 2 == 1
            
            // Bit 2 (0x04): Treatment halted - Check with division
            var treatmentHalted = (statusValue / 4) % 2 == 1
            
            // Update status items
            if (hasAlarm) {
                Pool_Has_General_Alarm.postUpdate(ON)
                Pool_Alarm.postUpdate(ON)
            } else {
                Pool_Has_General_Alarm.postUpdate(OFF)
            }
            
            if (treatmentHalted) {
                Pool_Treatment_Halted.postUpdate(ON)
            } else {
                Pool_Treatment_Halted.postUpdate(OFF)
            }
            
            // Update general status text
            var modeString = "Normal"
            if (hasAlarm) {
                modeString = "Alarm"
            }
            if (treatmentHalted) {
                modeString = "Treatment halted"
            }
            
            Pool_Status.postUpdate(modeString)
        }
    } else {
        logInfo("PoolSensors", "Alarm change ignored, as sensor values are not yet valid (pump running too short)")
    }
end

/**
 * Salt status evaluation according to register 0xC0
 * Uses modulo operation instead of bitmasks
 */
rule "Evaluate salt status"
when
    Item Pool_Salt_Status received update
then
    // Process salt status only when the pump is running and sensors are valid
    if (Pool_Sensors_Valid.state == ON) {
        val statusValue = (Pool_Salt_Status.state as Number).intValue
        
        // Bit 0 (0x01): Current too low - Check by modulo 2 (odd number)
        var currentTooLow = statusValue % 2 == 1
        Pool_Salt_Current_Too_Low.postUpdate(if (currentTooLow) ON else OFF)
        
        // Bit 1 (0x02): Measurement unreliable - (statusValue / 2) % 2
        var measureUnreliable = (statusValue / 2) % 2 == 1
        Pool_Salt_Measure_Unreliable.postUpdate(if (measureUnreliable) ON else OFF)
        
        // Bit 2 (0x04): Voltage too low - (statusValue / 4) % 2
        var voltageTooLow = (statusValue / 4) % 2 == 1
        Pool_Salt_Voltage_Too_Low.postUpdate(if (voltageTooLow) ON else OFF)
        
        // Bit 15 (0x8000): Calibration OK - (statusValue / 32768) % 2
        var calibrationOk = (statusValue / 32768) % 2 == 1
        Pool_Salt_Calibration_OK.postUpdate(if (calibrationOk) ON else OFF)
        
        // Update status message in the Salt_Status Item
        if (currentTooLow || measureUnreliable || voltageTooLow) {
            Pool_Salt_Statustext.postUpdate("Measurement error")
            
            // Since the measurement is unreliable, set the salt level to NULL
            if (Pool_Salinity.state !== NULL) {
                Pool_Salinity.postUpdate(NULL)
            }
            
            var errorMessage = ""
            if (currentTooLow) errorMessage += "Current too low, "
            if (measureUnreliable) errorMessage += "Measurement unreliable, "
            if (voltageTooLow) errorMessage += "Voltage too low"
            
            logWarn("PoolControl", "Salt measurement problems: " + errorMessage.trim)
        } else if (calibrationOk) {
            Pool_Salt_Statustext.postUpdate("OK (calibrated)")
        } else {
            Pool_Salt_Statustext.postUpdate("OK")
        }
    }
### 3. Pool_output.rules - Pump Control

This rule file handles the pool pump control via Modbus output register:

```java
// Idegis Domotic 2 Pool - Pump control via Modbus on Output register 1
// ===============================================================

/**
 * Simplified rules for controlling pool output 1 (pool pump)
 * Supports three modes:
 * - Off: Segment 4 = 0
 * - Manual On: Segment 4 = 4
 * - Automatic: Segment 4 = 8
 */


// === READ OUTPUT REGISTER ===
rule "Pool Read Output Register"
when
    Item Pool_Output1_Control received update
then
    val outputValue = (Pool_Output1_Control.state as DecimalType).intValue
    
    // Extract segment 4 (bits 12-15, highest nibble)
    // outputValue / 4096 gives us the direct value of segment 4
    val segment4 = outputValue / 4096
    
    // Check for automatic mode (segment 4 = 8)
    var isAutoMode = (segment4 == 8)
    
    // Check for manual mode (segment 4 = 4)
    var isManualMode = (segment4 == 4)
    
    // Update control switches
    if (isAutoMode) {
        Pool_Output1_InterneAutomatik.postUpdate(ON)
    } else {
        Pool_Output1_InterneAutomatik.postUpdate(OFF)
    }
    
    if (isManualMode) {
        Pool_Output1_An.postUpdate(ON)
    } else {
        Pool_Output1_An.postUpdate(OFF)
    }
end

// === SET MANUAL MODE ===
rule "Pool Set Manual Mode"
when
    Item Pool_Output1_An received command
then
    // Read current register value
    if (Pool_Output1_Control.state === NULL) {
        logError("Pool_Output", "Pool controller not available. Cannot switch via Modbus.")
        return
    }
    
    val currentValue = (Pool_Output1_Control.state as DecimalType).intValue
    
    // Remove segment 4 (bits 12-15): currentValue % 4096 gives the value without segment 4
    val lowerBits = currentValue % 4096
    
    // Add new segment 4: For ON set 4 (manual mode), for OFF set 0 (off)
    var newValue = lowerBits
    if (receivedCommand == ON) {
        // Activate manual mode (segment 4 = 4)
        newValue = lowerBits + 4 * 4096
        logInfo("Pool_Output", "Pool pump was set to manual continuous operation")
    } else {
        // Deactivate mode (segment 4 = 0)
        logInfo("Pool_Output", "Manual continuous operation of the pool pump has been turned off")
    }
    
    // Send value to Modbus register
    Pool_Output1_Control.sendCommand(newValue)
    
    // If manual mode is turned on, make sure the automatic status is updated
    if (receivedCommand == ON) {
        Pool_Output1_InterneAutomatik.postUpdate(OFF)
    }
end
 
// === SET AUTOMATIC MODE ===
rule "Pool Set Automatic Mode"
when
    Item Pool_Output1_InterneAutomatik received command
then
    // Read current register value
    if (Pool_Output1_Control.state === NULL) {
        logError("Pool_Output", "Pool controller not available. Cannot change mode.")
        return
    }
    
    val currentValue = (Pool_Output1_Control.state as DecimalType).intValue
    
    // Remove segment 4 (bits 12-15): currentValue % 4096 gives the value without segment 4
    val lowerBits = currentValue % 4096
    
    // Add new segment 4: For ON set 8 (automatic mode), for OFF set 0 (off)
    var newValue = lowerBits
    if (receivedCommand == ON) {
        // Activate automatic mode (segment 4 = 8)
        newValue = lowerBits + 8 * 4096
        logInfo("Pool_Output", "Pool pump has been switched to automatic mode")
    } else {
        // Deactivate mode (segment 4 = 0)
        logInfo("Pool_Output", "Automatic control of the pool pump has been turned off")
    }
    
    // Send value to Modbus register
    Pool_Output1_Control.sendCommand(newValue)
    
    // If automatic mode is turned on, make sure the manual status is updated
    if (receivedCommand == ON) {
        Pool_Output1_An.postUpdate(OFF)
    }
end

Advanced Usage and Applications

While this tutorial focuses on the core integration of the Idegis Domotic 2 LS with OpenHAB, there are many advanced applications that build on this foundation. In my home setup, I’ve implemented features such as:

  • Intelligent pool pump control based on PV (photovoltaic) surplus from my solar system
  • Pool heating control synchronized with energy availability
  • Long-term statistics and trend visualization
  • Integration with home automation scenes (e.g., vacation mode)

These advanced functionalities aren’t covered in this tutorial but show what’s possible once you have the basic pool controller integration working.

Modbus Register Bit Access in OpenHAB

In the rules, I’m using simple arithmetic operations with modulo and division instead of bitwise operators. I chose this approach because I had some issues with bitwise operators in the OpenHAB Rules DSL. The approach works as follows:

  • To check if bit 0 is set: value % 2 == 1 (checks if odd)
  • To check if bit 1 is set: (value / 2) % 2 == 1
  • To check if bit 2 is set: (value / 4) % 2 == 1

This pattern continues for higher bits by increasing the divisor.

Sensor Validation Logic

One important aspect of the implementation is the sensor validation logic. Pool sensors often provide unreliable readings when the pump is off or has just started. The rules include a mechanism to:

  1. Mark sensors as invalid when the pump is first turned on
  2. Wait for 3 minutes of pump operation
  3. Only then start using and displaying the sensor values

This prevents false readings and alarms during the pump startup phase.

Troubleshooting

If you encounter issues with the integration, here are some common troubleshooting steps:

  1. Serial Connection Issues: Verify your serial adapter is correctly recognized by your system
  2. Modbus Communication Errors: Set the timeBetweenTransactionsMillis to a higher value like 100ms
  3. Invalid Readings: Ensure the correct register addresses are used and the correct transformation is applied
  4. No Response: Check the Modbus slave ID (default 1, but configurable)

Conclusion

This tutorial provides a complete integration of the Idegis Domotic 2 LS pool controller with OpenHAB. With this setup, you can monitor all important pool parameters, control the pool pump, and build more complex automation based on the provided data.

The manufacturer was helpful in providing the Modbus register documentation, which simplified the implementation process. The main effort was in interpreting the values and translating the status bits into user-friendly information.

While the specific Modbus addresses might differ slightly based on the controller firmware version or model, the overall approach should work for any Idegis Domotic pool controller. The modular design of the Things, Items, and Rules makes it easy to adapt to different configurations.

Feel free to share your experiences or ask questions in the comments section below. Happy pool automation!.toString