BWT - Best Water Tech­nology - Support

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

1 Like

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. :person_shrugging:

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.

1 Like

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.

1 Like

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 some screenshots I managed to get from BWT UK customer support.

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) :white_check_mark: Confirmed: Matches your local time (BST = UTC+1).
3 40 Current Minute (UTC) :white_check_mark: Confirmed.
4 300 Water Hardness (ppm) :white_check_mark: Matches cloud data (“Hardness”: 300).
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) :white_check_mark: Confirmed: You set this manually to 02:00.
8 0 Recharge Time - Minute (UTC) :white_check_mark: Confirmed: Matches recharge setting.
9 9 Unknown Might relate to recharge interval or other config.
10 8 Base Model Number :white_check_mark: Matches logs and cloud telemetry.
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 :white_check_mark: Confirmed. Used in flow metering.
14 109 Avg. Water Served per Day (litres) :white_check_mark: Confirmed. Matches cloud data.
15 426 Total Water Served (litres) :white_check_mark: Confirms summary message (totalServWater)
16 0 Failed Transfer Attempts :white_check_mark: Confirmed from cloud data.
17 387 Days in Service :white_check_mark: Matches daysInServ in cloud data.
18 1803 Warranty Days Remaining :white_check_mark: Matches cloud info.
19 22 Total Number of Recharges :white_check_mark: Confirmed.
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 (%) :white_check_mark: Confirmed.
23 1204 Remaining Capacity (litres) :white_check_mark: Matches remCap from cloud messages.
24 11 Possibly Flow Rate or usage counter Matches historical patterns.
25 20 Dwell Duration (minutes) :white_check_mark: Seen in debug logs.
26 50 Brine Duration (minutes) :white_check_mark: Confirmed.
27 1 Allow Changing Salt Type Flag :white_check_mark: Confirmed (1 = allowed).
28 1 Allow Changing Regen Time Flag :white_check_mark: Confirmed.
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 :white_check_mark: Confirmed (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) :white_check_mark: Confirmed: 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

1 Like

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.
1 Like

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.
1 Like

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