Judo Softwell K for Openhab / descaling system

Hi there,

there was no possibity to access my descaling system’s status information. Even the manufacturer didn’t provide any usful api or whatsoever.

So this is my attempt on getting all the necessary information from my system (made by the german manufacturer “Judo”). This has only been tested with my “Softwell K” system and working fine for the last couple of weeks.

Please configure your knm token and serial number as mentioned in the code.

The code is using the same interface as the official judo app or webpage (reverse engineered). So it’s very unlikly that it could break (but not impossible).

Cu

PHP-Script for your server

##
## judo // JSON state fetcher
## v0.3a
## written by theLex, April 2020
##
## This has been developed for the Judo - SOFTwell K descaling system - and has only been tested with this one!
##
## Feel free to use this code and change it - but keep my credentials in the code!
## This code as been produced by reengineering the code behind www.myjudo.eu. This might stop working if the page is ever been changed.
## Use at your own risk. The script might run a while (has to request data first)
## Have fun :-)
##
## Usage:
##  1. http://192.xxx.xxx.xx/judo.php?load=day&do=preload
##  2. Wait 10 seconds
##  3. http://192.xxx.xxx.xx/judo.php?load=day
$version = "0.3a";


# Setup
$knm_token = ""; # your knm-token: you'll find it by logging into http://www.myjudo.eu. After that you'll find it in the browsers url
$serialnumber = ""; # there are different ways - the best is to browse your Softwell K's IP. There you'll find it on the front page

$day_to_load = date("jmY"); # you can change this if you want to retrieve the information for a different day
$week_to_load = date("WY"); # you can change this if you want to retrieve the information for a different week
$month_to_load = date("nY"); # you can change this if you want to retrieve the information for a different month
$year_to_load = date("Y"); # you can change this if you want to retrieve the information for a different year




# let's begin
# Building links
$url_getchartdata_day = "https://www.myjudo.eu/interface/?token=".$knm_token."&group=register&command=get_chart_data&serialnumber=".$serialnumber."&date=".$day_to_load."&parameter=day";
$url_getchartdata_week = "https://www.myjudo.eu/interface/?token=".$knm_token."&group=register&command=get_chart_data&serialnumber=".$serialnumber."&date=".$week_to_load."&parameter=week";
$url_getchartdata_year = "https://www.myjudo.eu/interface/?token=".$knm_token."&group=register&command=get_chart_data&serialnumber=".$serialnumber."&date=".$year_to_load."&parameter=year";
$url_getchartdata_month = "https://www.myjudo.eu/interface/?token=".$knm_token."&group=register&command=get_chart_data&serialnumber=".$serialnumber."&date=".$month_to_load."&parameter=month";
$url_getdevicedata = "https://www.myjudo.eu/interface/?token=".$knm_token."&group=register&command=get%20device%20data";
$loadNow = true;
$storeJSON = true;
$result = ["now" => date("d.m.Y H:i:s"), "fetcher_version" => $version, "msg" => "", "state" => "failed", "cached" => false, "urls" => []];

# which consumption informations do we want load?
$load = (isset($_GET['load']) ? strtolower(trim($_GET['load'])) : "day,month,week,year,device");
$do = (isset($_GET['do']) ? strtolower(trim($_GET['do'])) : "load");


# prepare result array
$result['msg'] = "okay";
$result['do'] = $do;
$result['state'] = "okay";
$result['load'] = $load;
$result['device'] = array();



if ($loadNow || $do == "preload") {
	$chartdata_week_string = "";
	$chartdata_day_string = "";
	$chartdata_month_string = "";
	$chartdata_year_string = "";
	

	# Chart data // year
	if (strpos($load, "year") !== false) {
		$lastState = "";
		$result['runner']['year'] = 0;
		while($lastState != "ok") {
			$chartdata_year_string = file_get_contents($url_getchartdata_year);
			$chartdata_year = json_decode($chartdata_year_string);
			$lastState = $chartdata_year->status;
			$result['runner']['year']++;
			if ($do == "preload") break;
			sleep(2);
			if ($result['runner']['year'] > 20) {
				break;
			}
		}
		$result['urls']['chart_year'] = $url_getchartdata_year;
		
		if ($storeJSON) file_put_contents("chartdata_year.json", $chartdata_year_string);
	}
	
	

	# Chart data // month
	if (strpos($load, "month") !== false) {
		$lastState = "";
		$result['runner']['month'] = 0;
		while($lastState != "ok") {
			$chartdata_month_string = file_get_contents($url_getchartdata_month);
			$chartdata_month = json_decode($chartdata_month_string);
			$lastState = $chartdata_month->status;
			$result['runner']['month']++;
			if ($do == "preload") break;
			sleep(2);
			if ($result['runner']['month'] > 20) {
				break;
			}
		}
		$result['urls']['chart_month'] = $url_getchartdata_month;
		
		if ($storeJSON) file_put_contents("chartdata_month.json", $chartdata_month_string);
	}
	
	
	# Chart data // day
	if (strpos($load, "day") !== false) {
		$lastState = "";
		$result['runner']['day'] = 0;
		$result['urls']['chart_day'] = $url_getchartdata_day;
		while($lastState != "ok") {
			$chartdata_day_string = file_get_contents($url_getchartdata_day);
			$chartdata_day = json_decode($chartdata_day_string);
			//ep($chartdata_day, $url_getchartdata_day);
			$lastState = $chartdata_day->status;
			$result['runner']['day']++;
			if ($do == "preload") break;
			sleep(2);
			if ($result['runner']['day'] > 20) {
				break;
			}
		}
		

		if ($storeJSON) file_put_contents("chartdata_day.json", $chartdata_day_string);
	}


	# Chart data // week
	if (strpos($load, "week") !== false) {
		$lastState = "";
		$result['runner']['week'] = 0;
		while($lastState != "ok") {
			$chartdata_week_string = file_get_contents($url_getchartdata_week);
			$chartdata_week = json_decode($chartdata_week_string);
			$lastState = $chartdata_week->status;
			$result['runner']['week']++;
			if ($do == "preload") break;
			sleep(2);
			if ($result['runner']['week'] > 20) {
				break;
			}
		}
		$result['urls']['chart_week'] = $url_getchartdata_week;
		
		if ($storeJSON) file_put_contents("chartdata_week.json", $chartdata_week_string);
	}


	# Device data
	if (strpos($load, "device") !== false && $do != "preload") {
		$getdevicedata_string = file_get_contents($url_getdevicedata);
		file_put_contents("getdevicedata.json", $getdevicedata_string);
		$result['urls']['deviceData'] = $url_getdevicedata;
	}
	
	
	if ($do == "preload") {
		$result['do'] = $do;
	}
	
	
} else {
	$chartdata_day_string = @file_get_contents("chartdata_day.json");
	$chartdata_week_string = @file_get_contents("chartdata_week.json");
	$chartdata_month_string = @file_get_contents("chartdata_month.json");
	$chartdata_year_string = @file_get_contents("chartdata_year.json");
	
	$getdevicedata_string = @file_get_contents("getdevicedata.json");
	$result['cached'] = true;
	
}


# Start decoding
if ($do != "preload") {
	if (strpos($load, "device") !== false) {
		$deviceData = json_decode($getdevicedata_string);
		if (is_object($deviceData) && isset($deviceData->status) && $deviceData->status == "ok") {
			if (isset($deviceData->data[0])) {
				
				
				# Standard information
				$result['device']['token'] = $deviceData->token;
				$result['device']['serialnumber'] = $deviceData->data[0]->serialnumber;
				$result['device']['status'] = $deviceData->data[0]->status;
				
				$result['device']['hardware_version'] = $deviceData->data[0]->data[0]->hv;
				$result['device']['software_version'] = $deviceData->data[0]->data[0]->sv;
				
				$result['device']['ewac_hardware_version'] = $deviceData->data[0]->hv;
				$result['device']['ewac_software_version'] = $deviceData->data[0]->sv;
				$result['device']['last_update'] = $deviceData->data[0]->data[0]->data->{"lu"};
				
				# Installation date
				$string = hexdec($deviceData->data[0]->data[0]->data->{"6"}->data);
				$result['device']['installation_date'] = date("d.m.Y H:i", $string);
				$result['device']['installation_date_timestamp'] = $string;
				
				# Device errors
				$result['device']['hasDeviceErrors'] = (isset($deviceData->data[0]->errors) && is_array($deviceData->data[0]->errors) && count($deviceData->data[0]->errors) > 0);
				$result['device']['deviceErrors'] = $deviceData->data[0]->errors;
				
				# device status (regeneration or not)
				$string = $deviceData->data[0]->data[0]->data->{"791"}->data;
				$string = explode(":", $string);
				$flag = hexdec(substr($string[1], 0, 2));
				if ($flag == 128) {
					$result['device']['device_status'] = "normal";
				} else {
					$result['device']['device_status'] = "regeneration";
				}
				
				
				# device number_format
				$string = $deviceData->data[0]->data[0]->data->{"3"}->data;
				$v1 = substr($string, 0, 2);
				$v2 = substr($string, 2, 2);
				$v3 = substr($string, 4, 2);
				$v4 = substr($string, 6, 2);
				$result['device']['device_number'] = hexdec("$v4$v3$v2$v1");
				
				
				# Hours til next service
				$string = $deviceData->data[0]->data[0]->data->{"7"}->data;
				$v1_high = substr($string, 0, 2);
				$v1_low = substr($string, 2, 2);
				$result['device']['next_service_in_days'] = floor(hexdec($v1_low.$v1_high)/24);
				
				# service counter
				$string = $deviceData->data[0]->data[0]->data->{"7"}->data;
				$v2_high = substr($string, 4, 2);
				$v2_low = substr($string, 6, 2);
				$result['device']['service_counter'] = hexdec("$v2_low$v2_high");
				
				# amount of handled water
				$string = $deviceData->data[0]->data[0]->data->{"9"}->data;
				$v1 = substr($string, 0, 2);
				$v2 = substr($string, 2, 2);
				$v3 = substr($string, 4, 2);
				$v4 = substr($string, 6, 2);
				$result['device']['liters_of_handled_water_total'] = hexdec("$v4$v3$v2$v1");

				# regeneration counter
				$string = $deviceData->data[0]->data[0]->data->{"791"}->data;
				$string = explode(":", $string);
				$lo = substr($string[1], 60, 2);
				$hi = substr($string[1], 62, 2);
				$result['device']['regeneration_counter'] = hexdec("$hi$lo");
			} else {
				$result['msg'] = "unable to find device data";
				$result['state'] = "error";
			}
		} else {
			$result['msg'] = "State not found / okay (".(isset($deviceData->status) ? $deviceData->status : "").")";
			$result['state'] = "error";
		}
	}
			
			
	$result['waterConsumption']['total'] = [];
	$result['waterConsumption']['loaded'] = [];

	# Water consumption // day
	if (strpos($load, "day") !== false) {
		$chartdata_day = json_decode($chartdata_day_string);
		$result['waterConsumption']['total']['day'] = 0;
		$result['waterConsumption']['loaded']['day'] = $day_to_load;
		$labels = ["00:00-03:00", "03:00-06:00", "06:00-09:00", "09:00-12:00", "12:00-15:00", "15:00-18:00", "18:00-21:00", "21:00-00:00"];
		
		$result['waterConsumption']['day'] = array();
		if ($chartdata_day->status == "ok") {
			if (strlen($chartdata_day->data) == 64) {
				foreach($labels as $index => $label) {
					$subIndex = ($index*8);
					$result['waterConsumption']['day'][$index] = [
																	"label" => $label,
																	"consumption" => hexdec(substr($chartdata_day->data, $subIndex, 8))
																  ];
					$result['waterConsumption']['total']['day'] +=  $result['waterConsumption']['day'][$index]['consumption'];
				}
			} else {
				# something is wrong!
				$result['msg'] = "Unable to decode day water consumption. String should be 64 characters long, but is ".strlen($chartdata_day->data);
				$result['state'] = "error";
			}
		}
	}

	# Water consumption // week
	if (strpos($load, "week") !== false) {
		$chartdata_week = json_decode($chartdata_week_string);
		$result['waterConsumption']['total']['week'] = 0;
		$result['waterConsumption']['loaded']['week'] = $week_to_load;
		$labels = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
		
		$result['waterConsumption']['week'] = array();
		if ($chartdata_week->status == "ok") {
			if (strlen($chartdata_week->data) == 56) {
				foreach($labels as $index => $label) {
					$subIndex = ($index*8);
					$result['waterConsumption']['week'][$index] = [
																	"label" => $label,
																	"consumption" => hexdec(substr($chartdata_week->data, $subIndex, 8))
																  ];
					$result['waterConsumption']['total']['week'] +=  $result['waterConsumption']['week'][$index]['consumption'];
				}
			} else {
				# something is wrong!
				$result['msg'] = "Unable to decode week water consumption. String should be 56 characters long, but is ".strlen($chartdata_week->data);
				$result['state'] = "error";
			}
		}
	}



	# Water consumption // month
	if (strpos($load, "month") !== false) {
		$chartdata_month = json_decode($chartdata_month_string);
		$result['waterConsumption']['total']['month'] = 0;
		$result['waterConsumption']['loaded']['month'] = $month_to_load;
		$labels = ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31"];
		
		$result['waterConsumption']['month'] = array();
		if ($chartdata_month->status == "ok") {
			if (strlen($chartdata_month->data) == 248) {
				foreach($labels as $index => $label) {
					$subIndex = ($index*8);
					$result['waterConsumption']['month'][$index] = [
																	"label" => $label,
																	"consumption" => hexdec(substr($chartdata_month->data, $subIndex, 8))
																  ];
					$result['waterConsumption']['total']['month'] +=  $result['waterConsumption']['month'][$index]['consumption'];
				}
			} else {
				# something is wrong!
				$result['msg'] = "Unable to decode month water consumption. String should be 248 characters long, but is ".strlen($chartdata_month->data);
				$result['state'] = "error";
			}
		}
	}


	# Water consumption // year
	if (strpos($load, "year") !== false) {
		$chartdata_year = json_decode($chartdata_year_string);
		$result['waterConsumption']['total']['year'] = 0;
		$result['waterConsumption']['loaded']['year'] = $year_to_load;
		$labels = ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"];
		
		$result['waterConsumption']['year'] = array();
		if ($chartdata_year->status == "ok") {
			if (strlen($chartdata_year->data) == 96) {
				foreach($labels as $index => $label) {
					$subIndex = ($index*8);
					$result['waterConsumption']['year'][$index] = [
																	"label" => $label,
																	"consumption" => hexdec(substr($chartdata_year->data, $subIndex, 8))
																  ];
					$result['waterConsumption']['total']['year'] +=  $result['waterConsumption']['year'][$index]['consumption'];
				}
			} else {
				# something is wrong!
				$result['msg'] = "Unable to decode year water consumption. String should be 96 characters long, but is ".strlen($chartdata_year->data);
				$result['state'] = "error";
			}
		}
	}
}


header("Content-Type: application/json");
echo json_encode($result);

Rule

rule "Entkalkungsanlage"
when
     Time cron "5 20,40,1 6-22 * * ?"
     or System started
then

    sendHttpGetRequest("http://192.xxx.xxx.xxx/judo.php?load=device,day&do=preload");

    shortTimer = createTimer(now.plusSeconds(10), [|
		var String json = sendHttpGetRequest("http://192.xxx.xxx.xxx/judo.php?load=device,day");

		postUpdate(Entkalk_Verbrauch_Heute, transform("JSONPATH", "$.waterConsumption.total.day", json));
		
		postUpdate(Entkalk_Fetcher_Status, transform("JSONPATH", "$.state", json));
		postUpdate(Entkalk_Fetcher_Now, transform("JSONPATH", "$.now", json));
		postUpdate(Entkalk_Info_token, transform("JSONPATH", "$.device.token", json));
		postUpdate(Entkalk_Info_status, transform("JSONPATH", "$.device.status", json));
		postUpdate(Entkalk_Info_LastUpdate, transform("JSONPATH", "$.device.last_update", json));
		postUpdate(Entkalk_Info_InstallDate, transform("JSONPATH", "$.device.installation_date", json));
		postUpdate(Entkalk_Info_NextServiceInDays, transform("JSONPATH", "$.device.next_service_in_days", json));
		postUpdate(Entkalk_Info_ServiceCounter, transform("JSONPATH", "$.device.service_counter", json));
		postUpdate(Entkalk_Info_LitersTotal, transform("JSONPATH", "$.device.liters_of_handled_water_total", json));
		postUpdate(Entkalk_Info_RegenerationCounter, transform("JSONPATH", "$.device.regeneration_counter", json));
		
		var String value = transform("JSONPATH", "$.device.hasDeviceErrors", json);
		if (value == "false") {
			postUpdate(Entkalk_Info_HasErrors, OFF);
		} else {
			postUpdate(Entkalk_Info_HasErrors, ON);
		}
			

		logInfo("Entkalkungsanlage_hourly", "Entkalkungsanlage aktualisiert. Wasserverbrauch: " + transform("JSONPATH", "$.waterConsumption.total.day", json) + " Liter. Fehler vorhanden: " + value);

		shortTimer = null;
	]);

end

Items

Group Entkalkungsanlage

String Entkalk_Verbrauch_Heute "Wasserverbrauch (heute) [%s l]" <water> (Entkalkungsanlage)
String Entkalk_Verbrauch_Woche "Wasserverbrauch (Woche) [%s l]" <water> (Entkalkungsanlage)
String Entkalk_Verbrauch_Monat "Wasserverbrauch (Monat) [%s l]" <water> (Entkalkungsanlage)

String Entkalk_Fetcher_Status "Fetcher Status [%s]" (Entkalkungsanlage)
String Entkalk_Fetcher_Now "Zeitpunkt [%s]" (Entkalkungsanlage)
String Entkalk_Info_LastUpdate "Letztes Update [%s]" (Entkalkungsanlage)
String Entkalk_Info_token "Token [%s]" (Entkalkungsanlage)
String Entkalk_Info_status "Status [%s]" (Entkalkungsanlage)
String Entkalk_Info_InstallDate "Installationsdatum [%s]" (Entkalkungsanlage)
Switch Entkalk_Info_HasErrors "Fehler gefunden [%s]" (Entkalkungsanlage)
Number Entkalk_Info_NextServiceInDays "Nächster Service [%d Tage]" (Entkalkungsanlage)
Number Entkalk_Info_ServiceCounter "Service counter [%d]" (Entkalkungsanlage)
Number Entkalk_Info_LitersTotal "Liter insgesamt [%d l]" <water> (Entkalkungsanlage)
Number Entkalk_Info_RegenerationCounter "Regeneration counter [%d]" (Entkalkungsanlage)

Sitemap

Text item=Entkalk_Verbrauch_Heute
Text item=Entkalk_Verbrauch_Woche
Text item=Entkalk_Verbrauch_Monat

Group label="Weitere Informationen" item=Entkalkungsanlage

Hey @lex, thanks for info. I had a technician with me today who referenced Judo as raising manufacturer for descaling systems. On the Judo webpage I saw that the Softwell devices only have “optional connectivity module”. I guess you have to purchase that one in addition, right?
From you script, seems like the device is then pushing data to the cloud and one has to poll the cloud instead of the local device. Did you try to connect locally as well? Or rerouting myjudo.eu traffic from the device to a local endpoint?
Thanks for this great start with Judo devices.

This could be worth a look:
https://wiki.fhem.de/wiki/JUDO_iSoft_Plus

1 Like

Hi there, sorry for my late reply.

Yes, you need this connectivity module. The script really connects to the cloud (after the device has pushed the data there).

Yes, I’ve given it a (short) try to read the data from the device - without success. Officially there is no possibity to get the desired data by API. So mine seems to be the (currently) only way.

iSoftPlus is different to SoftWell K - my script doesn’t work with this one - but could be changed/adapted.

Hello,
i also use a judo softwell water softening system. Is there a binding or another way to integrate the system into openhab3.

Basically, the display of the status or the daily water consumption would be enough for me.

Thanks very much