Water softener (CLACK WS1) consumed m3/liter monitoring

@mzagula78
I made a pcb with esp32, which readouts the two relays on a 5 button WS1 (15VDC version)
See my github and documents (stil working a bit on a finalized software version in two languages, dutch and english, finished in a couple of days)

With salt sensor
Favorite esp32 will be a wemos s3 mini because of the smaller size and better fit inside the clack housing.



1 Like

perfect! good stuff. I am planning to do this with Z-uno board integrated with HA and also with ultrasonic sensor for salt level measurement. Are you plannign to release this custom integration to HA? thanks

The esphome code is already on github.
No need for integration as it is esphome. And a bit of custom yaml in HA itself (working on this one)

Great work!

I dont have a Clack, mine is a Pentair BINRUN (rebadged/modified to FLECK 5600sxt); I am thinking how to adapt your code to fit this model as well.

Fantastic idea.

What functionality requires the relay connections - reading the current state (regen or not)? Unfortunately, my two WS1s donā€™t have a relay. Most of the remainder looks the same and Iā€™m assuming flow can be measured through the meter connector. Presumably the COMM CABLE connector can be used for something but I canā€™t find any actual documentation on what it does.

Iā€™m particularly interested in being able to monitor flow through two Clacks, plus the salt level in the brine tank.

1 Like

Relais1 is used to count the liters, during ā€œin serviceā€ of the Clack. Every 2 liters a puls is given.
Relais2 is used to give a puls at start regeneration.
If you donā€™t have this information, then my PCB is only a salt level monitoring. (DFrobot ultrasonic sensor)

I didnt find any documentation to make use of the com cable or connector J11

This is a picture of a pcb with the relais connector (3pin)

So the COMM port is for communicating with Clack System Controller V3030

Some PDFā€™s here Clack Corp Clack System Controller Assembly (No Cord) 530020084, Clack V3030-01 From Ā£10.80 - Wrekin Water Filtration Water Softener Parts but no info on protocol

One PDF says that it has CLK|DATA|GND connections. I measured 3v between DATA and GND. So you can actually collect all of the info from a single head and probably program regen cycles etc.

The problem - comm protocol is nowhere to be found (and that makes sense since they want to sell that controller which is $$$$)

1 Like

What kind of distance sensor is that in the picture under the lid? Does it come with the mounting bracket?

Its a uart DFrobot ultrasonic sensor
A02YYUW Waterproof Ultrasonic Distance Sensor (3~450cm, UART, IP67) - DFRobot
Code is in my github
I just use 2x M3 bolts and nuts to mount it.

For a new clack PCB design (just delivered today) i am now using a m5stack TOF sensor with i2c.
Its a little bit cheaper. But not so waterproof i guess.
Time-of-Flight Distance Ranging Sensor Unit (VL53L0X) | m5stack-store
New design PCB for controlling a chlorinator (aqmos.nl / aqmos.de) and measuring power/current/voltage with a ina219 module

Thanks. Iā€™m using one of the cheap ones with no waterproof at all, itā€™s working good and inexpensive to replace, but in case it fails yours seems a nice alternative.

Looks like the protocol is ModBus per this PDF: https://www.cwwltd.com/content/pdf/Clack%20Control%20Valves_Fittings%20and%20Accessories.pdf

And this PDF shows the ModBus registers for the WS3. Obviously there is not guarantee that those registers will be the same ones on the WS1 but it is a good starting point.
https://www.manualslib.com/manual/2917929/Water-Specialist-Ws2hf.html#manual

How can it be modbus tcp/ip if itā€™s three wires wheere two of them are marked as clk and data?

Ws3 has rj45 jack and that makes sense for modbus over tcp/ip

Modbus over serial aka rs-485? Maybe but calling pins CLOCK and DATA sounds rather steange and at least what iā€™ve seen that is usually 2 wires

Clock data hints a bit more to i2c perhaps?

Just conveying what the PDF documents say. Do agree with you that itā€™s most likely RS-485 or I2C. Gonna try sniffing the interface to see if I get something.

im interested in using that, too. any news on the comm port?

I hooked up a logic analyzerā€¦ there is nothing on those pinsā€¦ no talk at all.

I also tried to put the valve into controller mode but 1 Minute after setting controller mode it gave me an error code of 106. Had to power off the valve completely and had to set mode back to Alt off.

1 Like

Iā€™m pretty happy with water flow metering only. Though regen would be nice, but it can be calculated on hass side

Not the brightest idea but. Maybe we should just send a letter to Clack Corp.? Not all businesses are aā€¦les like mazda or myq ) There is not that much $ that can be made from enthusiasts of HA

Newbie here!
Here is my setup to monitor m3/liter continuously on the Clack WS1.
I am using the meter port (3 wires, red-black-white on the bottom right corner of the board), together with ESP32 module. I am using only the black wire (ground) and the white wire(signal). The signal has constant voltage of about 3.3v and when the water flow starts, it start pulsing (drops to 0v and then goes back up to 3.3v) depending of how fast the water flows. I figured (made measurements/calibration) that when 17 pulses has passed 1 liter of water is being pushed trough. I have a second Clack WS1 and it measures the same thing there as well. I connected the black wire to the ground of the ESP and white wire to pin 35. (powering the esp with external power supply). Here is the code for the ESP32:

#include <WiFi.h>
#include <HTTPClient.h>
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

#define water_pin 35
#define PULSES_PER_LITER 17
#define UPDATE_INTERVAL 60000  // 1 minute in milliseconds

int pulseCount = 0;
bool isLowVoltage = true;
float totalLiters = 0.0;
unsigned long lastUpdate = 0;

const char *ssid = "WiFi";
const char *password = "password";
const char *serverUrl = "http://10.1.1.151/water_meter.php";

void sendDataToServer(float liters) {
  // Check if liters is greater than 0 before sending to the server
  if (liters > 0.0) {
    HTTPClient http;

    // Prepare JSON payload
    String jsonData = "{\"liters\":" + String(liters, 3) + ", \"entity\":\"hallway\"}";


    // Send HTTP POST request to PHP script
    http.begin(serverUrl);
    http.addHeader("Content-Type", "application/json");
    int httpResponseCode = http.POST(jsonData);

    // Check for a successful response
    if (httpResponseCode == 200) {
      Serial.println("Data sent successfully");
    } else {
      Serial.print("HTTP Error code: ");
      Serial.println(httpResponseCode);
    }

    http.end();  // Close the connection
  }
}

void setup() {
  Serial.begin(115200);

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  Serial.println("Connected to WiFi");

  // Set totalLiters to 0 on startup
  totalLiters = 0.0;

  // Set the last update time
  lastUpdate = millis();

  // Configure OTA
  ArduinoOTA.setHostname("esp32-waterflow-hallway");
  ArduinoOTA.begin();
  ArduinoOTA.setPassword("");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
}

void loop() {
  // Handle OTA updates
  ArduinoOTA.handle();

  // Read the analog value from water_pin
  int analogValue = analogRead(water_pin);

  // Check if the analog value is zero
  if (analogValue == 0) {
    // If in a low voltage state, start a new pulse
    if (isLowVoltage) {
      pulseCount++;
      isLowVoltage = false;

      // Increment total liters for each pulse
      totalLiters += 1.0 / PULSES_PER_LITER;
      Serial.print("Total Liters: ");
      Serial.println(totalLiters, 3);
    }
  } else {
    // If the voltage is not zero and isLowVoltage was false, reset the flag
    if (!isLowVoltage) {
      isLowVoltage = true;
    }
  }

  // Check if it's time to update the database
  if (millis() - lastUpdate >= UPDATE_INTERVAL) {
    // Send data to server
    sendDataToServer(totalLiters);

    // Reset totalLiters
    totalLiters = 0.0;

    // Reset pulse count or perform any other actions needed before the next update

    // Set the last update time
    lastUpdate = millis();
  }

  delay(10);  // Optional delay to control the rate of analog readings
}

Let me briefly explain the code:
We are reading the pulses on the pin, collecting the data for 1 min, translating the pulses to liters, and then sending the data to a php file hosted on a local raspberry pi, after that the process starts over.
Here is how the php file looks like:

<?php
// Connect to MySQL
$host = 'localhost';
$dbname = 'db_name';
$username = 'username';
$password = 'password';

try {
    $db = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
    // Set the PDO error mode to exception
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    if ($_SERVER['REQUEST_METHOD'] === 'GET') {
        // Retrieve the entity variable from the GET request
        $entity = isset($_GET['entity']) ? $_GET['entity'] : null;
        if ($entity=='') {
            http_response_code(400);
            echo json_encode(array('message' => 'Entity cannot be empty'));
            die();
        }
        // Handle GET request to retrieve data
        try {
            // Example SQL query to retrieve data for a specific entity
            $stmt = $db->prepare("SELECT FORMAT(SUM(liters), 3) AS total_liters FROM data WHERE DATE(date) = CURDATE() AND entity = :entity");
            $stmt->bindParam(':entity', $entity);

            $stmt->execute();

            // Fetch all rows as associative arrays
            $result = $stmt->fetchAll(PDO::FETCH_ASSOC);

            // Return the result as JSON
            echo json_encode($result);
        } catch (PDOException $e) {
            // Log the error and send a response with an error message
            http_response_code(500);
            echo json_encode(array('message' => 'Internal Server Error.'));
        }
    } elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
        // Handle POST request to insert data
        $data = json_decode(file_get_contents("php://input"), true);
        
        
        if (isset($data['liters'])&& isset($data['entity'])) {
            $liters = $data['liters'];
            $entity = $data['entity'];

            // Begin a new transaction
            $db->beginTransaction();

            try {
                // Example SQL query to insert data
                $stmt = $db->prepare("INSERT INTO data (date, entity, liters) VALUES (NOW(), :entity, :liters)");
                $stmt->bindParam(':entity', $entity);
                $stmt->bindParam(':liters', $liters);

                // Execute the query
                $stmt->execute();

                // Commit the transaction
                $db->commit();

                echo json_encode(array('message' => 'Data inserted successfully'));
            } catch (PDOException $e) {
                // Rollback the transaction on error
                $db->rollBack();
                http_response_code(500);
                echo json_encode(array('message' => 'Internal Server Error.'));
            }
        } else {
            http_response_code(400);
            echo json_encode(array('message' => 'Bad request. Missing "liters" in the request.'));
        }
    } else {
        http_response_code(405); // Method Not Allowed
        echo json_encode(array('message' => 'Method not allowed.'));
    }
} catch (PDOException $e) {
    // Log the error and send a response with an error message
    http_response_code(500);
    echo json_encode(array('message' => 'Internal Server Error.'));
}

?>

Iā€™ll explain briefly the php file. It gets the data from the ESP and stores it in a database. Also Serves the stored data to Home Assistant.

Here is HA setup:
In configuration.yaml Iā€™ve added:

#Sensor for the water consumption in the hallway     
  - platform: rest
    name: Water Consumption Hallway
    resource: http://10.1.1.151/water_meter.php?entity=hallway
    value_template: "{{ value_json[0].total_liters | float }}"
    json_attributes:
      - entity
    unit_of_measurement: "L"
    scan_interval: 60  # Update every 60 seconds
    unique_id: sensor.water_consumption_hallway
    device_class: water
    state_class: total_increasing
#Sensor for the water consumption in the kitchen    
  - platform: rest
    name: Water Consumption Hallway
    resource: http://10.1.1.151/water_meter.php?entity=kitchen
    value_template: "{{ value_json[0].total_liters | float }}"
    json_attributes:
      - entity
    unit_of_measurement: "L"
    scan_interval: 65  # Update every 60 seconds
    unique_id: sensor.water_consumption_kitchen
    device_class: water
    state_class: total_increasing    
#Combine the two sensors    
  - platform: template
    sensors:
      total_water_consumption:
        friendly_name: "Total Water Consumption"
        value_template: "{{ states('sensor.water_consumption_kitchen') | float + states('sensor.water_consumption_hallway') | float }}"
        unit_of_measurement: "L"
        device_class: water
      total_water_cost:
        friendly_name: "Total Water Cost"
        value_template: "{{ ((states('sensor.water_consumption_kitchen') | float + states('sensor.water_consumption_hallway') | float) * 0.003498) | round(2) }}"
        unit_of_measurement: "BGN"
        device_class: water

p.s. 0.003498 is the cost for 1 liter of water where I live right now.

The sensor will also shows in the energy dashboard as well.
Here is the end result:

i wrote them an email and asked for support and maybe some documentation about communication on that portā€¦ iā€™m not super optimistic about sucess but heyā€¦ i cant be more than a no :wink:

1 Like

What is the easiest way to sense the relays state? Looking at the docs contacts are at 12VDC? Logic level conferter? Optocoupler? Or really put the relays inside?