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
Based on your input I created a beta version of the integration. The config flow tries both endpoints and then uses the one that is available.
What is currently unclear:
- It seems the data you reported are slightly different? One has 47, the other 48 items. Tbh I lost a bit the overview, the entities towards the end might be wrong if the length differs.
- I publish all liter values as I receive them (probably treated water). To calculate the blended water like I do for the other devices, I would need the hardness after the device or mix ratio.
- I publish all unclear values to entities silk_register_{index}. Hope this helps to find patterns behind the remaining values.
Thanks dkarv! I was going to do a PR tonight with something similar - I’ve created one with just a few tweaks instead.
Other notes:
- Last regeneration column 1 - this isn’t actually when it last regenerated, just the user-configured time when it will regenerate if it needs to (e.g. middle of the night so we dont draw hard water), so the sensor is a bit misleading.
- Similarly incoming water hardness is a user-configured value (not as misleading as above)
- The warranty dates etc should just be forward calculated (similar to Last regeneration column 1) as a timestamp in my opinion. HA will then display length of time til expiry nicely.
- I think Next Customer Service is when the @Home service agreement expires but I might be wrong
- I’ve reduced max poll time to 10 seconds to try to catch any flow rates faster but it’ll never be perfect.
- If you can be bothered, you can get the firmware version from /silk/status endpoint json (join version and gitver strings e.g. ‘{version} - {gitver}’ - that’s what is presented in the local web app)
- /silk/status also shows ‘connected’ status which you may want to present as an error
- I worked out the total capacity of my unit to be 2700 litres which lines up with what I saw after the regeneration process. This was done via the spec sheet https://www.bwt.com/en-gb/-/media/bwt/uk/downloads---current-models/technical-data-sheets/bwt-perla-silk.pdf?rev=469cc3abe6fa43cd96cc3d84fa6d680f - taking the ppm figure from the Nominal Capacity row (770 for me) and dividing by my inputted water hardness (285) then times 1000 - e.g. (770 / 285) * 1000 = 2701. You could present a % capacity used sensor if you hardcoded these values on a per-model (model is available on /silk/status) basis but probably not worth it.
Thanks for your hints.
Last regeneration column 1
Guess I will remove it then, don’t see how it is helpful at all
incoming water hardness
this is actually the same for the BWT Perla, but I include it for the calculations
warranty dates calculating forward
makes sense, will do
Next Customer Service
Maybe I adjust the translation there
max poll time
I experienced a lot with it in the past, and as you say it will never be perfect and shouldn’t be the input for any automations etc. Even the value in the api lags behind, so I will stay with 30 seconds for now to not generate too many requests.
/silk/status
I might consider this in the future, but it would need a few changes. So not top priority right now for me


