Yes I am in UK on 2.0307 - 157 and can’t find a way to enable local API
So how come I have 2.200148 and no one else. On my version I can query only 2 endpoints: /silk/status and /silk/registers. The later is the one that provides the data but it comes as JSON array of the values. So I need to reverse engineer what is what. Some items are easy to guess but others - no way.
Can you try /silk/status and /silk/registers. Both on port 80
I have emailed BWT support and the technical team said that the FW that’s on the UK devices does not provide local API.
I’m looking for a new water softener, but no local API means no purchase.
Brilliant- yes those endpoints work. How did you come across them?
My /registers is
{"params":[0,-1,18,46,285,29,16,2,0,9,8,1,8,4,297,150,0,50,2140,8,40,0,100,521,11,20,50,1,1,0,160,160,20,0,680,1,-1,-1,4,1,1,15,116,-1,-1,-1,-1,0]}
Array /silk/registers
indexes that I’ve managed to work out:
2 = Current time - Hour in UTC
3 = Current time - Minute in UTC
7 = Recharge time - Hour (?)
8 = Recharge time - Minute (?)
19 = Number of recharges Matches cloud API value:
"TotalRecharges": 9
14 = Water use - Litres - per day avg
15 = Water use? Mine says 159, which matches a value from cloud API that is
"TotalWaterServed": 15900.0
16 = Current flow rate (Litres per sec)
42 = Water use - Litres - resets to zero overnight at 2am (recharge time?)
23 = Current Capacity litres - mine goes to 2630 after recharge (Perla Silk M), could work out % from this - assume this changes per model.
34 = coverage remaining (warranty? In days) Matches cloud API value:
"DaysServiceExpire": 678
17 = Days in service (53 in my case) Matches cloud API value:
"TotalDaysInService": 53
18 = some high value in days (2137 in my case), counting down each night (note: [17] + [18] = 2190 = number of days in 6 years - what’s special about 6 years on these devices?)
23 = high value that varies - potential input hardness? I’m in area of hard water I think ~ roughly 280ish on mg/L - very rough and this is hovering around 2150-2300.
30 = fixed value of 160 - salt max capacity - This is in kg * 10 from the spec sheet. Mine is M and rated at 16kg of salt capacity. Matches cloud API value:
"MaxSaltCapacity": 16.0
31 = some value that only gets updated after recharge - was at 160 when at 100% salt, now 141 after recharge. Matches cloud API value:
"CurrentSaltLevelKg": 14.1
- App is showing 88% - 141/160 is 88% so [30] is max and [31] is salt remaining so [31] divided by [30] is salt % remaining
I went to /web/ and then noticed that this page is pulling data from those 2 endpoints.
Easy now you mention it - @otoo do you have any other indexes that you’ve worked out?
I will build a new (or add to the existing) BWT integration to support us on the newer firmwares.
Interesting. Is there any content on /web that indicates what the values are?
If not, more datapoints are needed to reverse engineer the api. Would be helpful if more people post their responses.
Probably the (in/out) hardness is in there, also probably errors encoded as numbers.
For the water use you need to figure out whether they relate to blended water or fully desalinated water - there was quite a bit of confusion when developing the original integration due to a mix of them in the api response.
Happy to assist or add the api to the integration once we figured out the details.
There is also a debug log view but I think this is just the ESP32 retrieving the underlying values from the hardware sensors etc
/silk/getdebug
[
"165651\t181008\t142460\t80320\tPolling register set 24, valid: 1, s: 0, e:0 ",
"165651\t181008\t142460\t80320\t[SoftenerComms.cpp:309] PollRegisters(): Sent command=$R1808",
"165651\t181008\t142460\t80320\t[SoftenerComms.cpp:710] ParseRegValsFromHost(): ParseRegValsFromHost\r\n Addr24,Count8 ",
"165651\t181008\t142460\t80320\t000B ",
"165651\t181008\t142460\t80320\t0014 ",
"165651\t179296\t142460\t80320\t0032 ",
"165651\t181008\t142460\t80320\t0001 ",
"165651\t181008\t142460\t80320\t0001 ",
"165651\t181008\t142460\t80320\t0000 ",
"165651\t181008\t142460\t80320\t00A0 ",
"165651\t181008\t142460\t80320\t008D ",
"165651\t181008\t142460\t80320\t\r\n",
"165653\t176872\t142460\t80320\tHandling web request: status",
"165653\t172080\t142460\t80320\t[SoftenerComms.cpp:288] FormatRegisterString(): FormatRegisterString: {\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-",
"165653\t169720\t142460\t80320\t{\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-1,-1,-1,0]}",
"165655\t167632\t142460\t80320\t[SoftenerComms.cpp:288] FormatRegisterString(): FormatRegisterString: {\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-",
"165655\t172076\t142460\t80320\t{\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-1,-1,-1,0]}",
"165655\t173940\t142460\t80320\tHandling web request: status",
"165656\t181004\t142460\t80320\t[SoftenerComms.cpp:119] ProcessSoftenerComms(): Polling register set 32",
"165656\t181004\t142460\t80320\tPolling register set 32, valid: 1, s: 0, e:0 ",
"165656\t181004\t142460\t80320\t[SoftenerComms.cpp:309] PollRegisters(): Sent command=$R2008",
"165656\t181004\t142460\t80320\tBOASTW3 is talking: 0014000002a600010001000000a0008d",
"165656\t181004\t142460\t80320\t[SoftenerComms.cpp:710] ParseRegValsFromHost(): ParseRegValsFromHost\r\n Addr32,Count8 ",
"165656\t181004\t142460\t80320\t0014 ",
"165656\t181004\t142460\t80320\t0000 ",
"165656\t181004\t142460\t80320\t02A6 ",
"165656\t181004\t142460\t80320\t0001 ",
"165656\t181004\t142460\t80320\tFFFF ",
"165656\t181004\t142460\t80320\tFFFF ",
"165656\t181004\t142460\t80320\t0006 ",
"165656\t181004\t142460\t80320\t0001 ",
"165656\t181004\t142460\t80320\t\r\n",
"165659\t169620\t142460\t80320\tHandling web request: status",
"165659\t168836\t142460\t80320\t[SoftenerComms.cpp:288] FormatRegisterString(): FormatRegisterString: {\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-",
"165659\t169708\t142460\t80320\t{\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-1,-1,-1,0]}",
"165661\t181008\t142460\t80320\t[SoftenerComms.cpp:119] ProcessSoftenerComms(): Polling register set 40",
"165661\t181008\t142460\t80320\t[SoftenerComms.cpp:309] PollRegisters(): Sent command=$R2808",
"165661\t177200\t142460\t80320\tHandling web request: status",
"165661\t172800\t142460\t80320\t[SoftenerComms.cpp:288] FormatRegisterString(): FormatRegisterString: {\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-",
"165661\t172112\t142460\t80320\t{\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-1,-1,-1,0]}",
"165661\t181008\t142460\t80320\t[SoftenerComms.cpp:710] ParseRegValsFromHost(): ParseRegValsFromHost\r\n Addr40,Count8 ",
"165661\t181008\t142460\t80320\t0001 ",
"165661\t181008\t142460\t80320\t000F ",
"165661\t181008\t142460\t80320\t015F ",
"165661\t181008\t142460\t80320\tFFFF ",
"165661\t181008\t142460\t80320\tFFFF ",
"165661\t181008\t142460\t80320\tFFFF ",
"165661\t181008\t142460\t80320\tFFFF ",
"165661\t181008\t142460\t80320\t0000 ",
"165661\t181008\t142460\t80320\t\r\n"
]
Nice one
I’ll continue to update the above post on indexes: I’ve worked out water use and salt % now so they’re the main ones I’d guess
Can you tell me what the native unit of measurement the BWT units tend to use for hardness? Is it dH or ppm or mg/L?
The Perla One and Duplex use dH everywhere. Edit: I checked and they actually publish 4 values: ppm caco3, dh, fH, mmol/l. I do calculations with dh, but guess it work with the others if they are different by just a factor.
They also have two different service counters, one performed by the customer twice per year, and the technician one every second year I think. But maybe the intervals are different, could be 6 years for yours.
BTW is there any authentication for these routes?
And have you figured out whether the water consumption is blended water (actually used liters in household) or fully 0dH water, like on the other Perla apis?
Totally unauthenticated.
The water consumption is just what goes through the softener unit - I haven’t configured any blending so my usage is pure soft - when I get a chance I’ll add some blend and see what the numbers diff by.
Here’s what I’m using currently (I can PR this to your repo when I’ve chance - would be good to build up a database of versions that support this endpoint!):
# BWT water softener
- scan_interval: 30
resource: http://192.168.7.51/silk/registers
# {"params":[0,-1,18,23,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2275,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,355,-1,-1,-1,-1,0]}
sensor:
# Water usage sensors
- name: "BWT Water Softener - Daily Average Water Use"
value_template: "{{ value_json.params[14] }}"
unit_of_measurement: "L"
device_class: water
icon: "mdi:water"
- name: "BWT Water Softener - Total Water Served"
value_template: "{{ (value_json.params[15] * 100) | round(0) }}"
unit_of_measurement: "L"
device_class: water
state_class: total_increasing
icon: "mdi:water"
- name: "BWT Water Softener - Today Water Use"
value_template: "{{ value_json.params[42] }}"
unit_of_measurement: "L"
device_class: water
icon: "mdi:water"
# Service and maintenance sensors
- name: "BWT Water Softener - Days in Service"
value_template: "{{ value_json.params[17] }}"
unit_of_measurement: "d"
device_class: duration
state_class: total_increasing
icon: "mdi:calendar-clock"
- name: "BWT Water Softener - Total Recharges"
value_template: "{{ value_json.params[19] }}"
unit_of_measurement: "recharges"
state_class: total_increasing
icon: "mdi:counter"
- name: "BWT Water Softener - Warranty Days Remaining"
value_template: "{{ value_json.params[34] }}"
unit_of_measurement: "d"
device_class: duration
state_class: measurement
icon: "mdi:shield-clock"
# Capacity and hardness sensors
- name: "BWT Water Softener - Current Capacity"
value_template: "{{ value_json.params[23] }}"
unit_of_measurement: "L"
device_class: water
icon: "mdi:water-percent"
- name: "BWT Water Softener - Current Capacity Percentage"
value_template: "{{ ((value_json.params[23] / 2630) * 100) | round(0) }}"
unit_of_measurement: "%"
state_class: measurement
icon: "mdi:water-percent"
# Salt level sensors
- name: "BWT Water Softener - Max Salt Capacity"
value_template: "{{ (value_json.params[30] / 10) | round(1) }}"
unit_of_measurement: "kg"
device_class: weight
icon: "mdi:shaker"
- name: "BWT Water Softener - Current Salt Level"
value_template: "{{ (value_json.params[31] / 10) | round(1) }}"
unit_of_measurement: "kg"
device_class: weight
state_class: measurement
icon: "mdi:shaker"
- name: "BWT Water Softener - Salt Level Percentage"
value_template: "{{ ((value_json.params[31] / value_json.params[30]) * 100) | round(0) }}"
unit_of_measurement: "%"
state_class: measurement
icon: "mdi:percent"
These are all my findings so far.
Index | Value | Meaning | Notes / Confirmation |
---|---|---|---|
0 | 0 | Salt Type | Likely: 0 = Tablet salt. Could be enum. |
1 | -1 | Unknown / Reserved | Possibly unused or uninitialized. |
2 | 23 | Current Hour (UTC) | ![]() |
3 | 40 | Current Minute (UTC) | ![]() |
4 | 300 | Water Hardness (ppm) | ![]() |
5 | 30 | Unknown – Possibly firmware or config ID | Needs further confirmation. |
6 | 17 | Unknown – Possibly status flags | May be a mode bitfield. |
7 | 2 | Recharge Time - Hour (UTC) | ![]() |
8 | 0 | Recharge Time - Minute (UTC) | ![]() |
9 | 9 | Unknown | Might relate to recharge interval or other config. |
10 | 8 | Base Model Number | ![]() |
11 | 1 | Duplex Setting | 1 = Duplex mode enabled? Confirmed in message logs. |
12 | 8 | Unknown | Possibly same as model ID. Needs clarification. |
13 | 4 | Turbine Pulses per Liter | ![]() |
14 | 109 | Avg. Water Served per Day (litres) | ![]() |
15 | 426 | Total Water Served (litres) | ![]() totalServWater ) |
16 | 0 | Failed Transfer Attempts | ![]() |
17 | 387 | Days in Service | ![]() daysInServ in cloud data. |
18 | 1803 | Warranty Days Remaining | ![]() |
19 | 22 | Total Number of Recharges | ![]() |
20 | 40 | Possibly recharge delay duration | Might also relate to service schedule. |
21 | 0 | Unknown | Could be error state, flag, or counter. |
22 | 100 | Salt Level (%) | ![]() |
23 | 1204 | Remaining Capacity (litres) | ![]() remCap from cloud messages. |
24 | 11 | Possibly Flow Rate or usage counter | Matches historical patterns. |
25 | 20 | Dwell Duration (minutes) | ![]() |
26 | 50 | Brine Duration (minutes) | ![]() |
27 | 1 | Allow Changing Salt Type Flag | ![]() |
28 | 1 | Allow Changing Regen Time Flag | ![]() |
29 | 0 | Unknown | Possibly related to regeneration or schedule. |
30 | 160 | Possibly Max Capacity or threshold | Needs monitoring. |
31 | 100 | Possibly Current Capacity % | Matches salt % at times. Needs clarification. |
32 | 20 | Possibly minimum brine amount | Needs checking. |
33 | 0 | Possibly Days Since Last Service | 0 = could mean just serviced or reset. |
34 | 343 | Days Until Next Service | ![]() daysServiceExp ). |
35–38 | varies | Unknown | Could be error codes, counters, EEPROM diagnostics |
39 | 1 | Possibly telemetry / cloud sync flag | Consistently 1 – transmission enabled? |
40 | 1 | Possibly telemetry / sync status | Similar to above. |
41 | 1 | Possibly ‘Registered’ flag | Consistent with /status.registered=true |
42 | 15 | Unknown | Could be flow metric or counter. |
43 | 57 | Daily Water Usage (litres) | ![]() WaterConsumption=57 from 1002 msg. |
44–47 | -1 | Reserved / Unused | Always -1. Uninitialized or inactive registers. |
48 | 0 | Unknown | Possibly system status or reserved. |
Nice one
[22] is not salt level for me - it is always 100 but the app shows 88% salt. This is worked out via [31]/[30] (remaining salt / max salt capacity)
[16] is current flow rate in L/s - not failed transfer attempts - I tested this morning empirically