Oras / Hansa / Amphiro digital shower head bluetooth connection

I recently got a Hansa digital shower head and it is working with the IOS app ok.

I downloaded this integration from HACS to try and read the shower head bluetooth output in Home Assistant: GitHub - chkuendig/hass-amphiro-ble: 🚿 Amphiro Digital Hand Shower component for Home Assistant

I have bluetooth enabled in Home Assistant. I am running HA in a Proxmox VM and bluetooth is passed through ok.

From reading github page and a few forum posts on the subject, it sounds like the shower head should autmoatically get picked up by the integration when it is running.

In my home assistant I have a Device called ā€œAmphiro Digital Hand Shower BLEā€ but no entities, and there isnt anything in Integrations.

The Device has nothing in there other than ā€˜Update’ in Configuration and that just displays a spinning circle like its waiting for something. I hoped that would change with the shower running, but it does not.

I had to pair the shower head with the app using a code displayed on the shower head screen, but i cant see any way of doing that in HA.

Im new to Home Assistant and im probably doing something stupid, but i cant get this integration to do anything. Can anyone advise please?

@Edwin_D

Hi. I did use the HA integration for some time, reporting the same name, connected via a bluetooth proxy. It didn’t need pairing, it just listens to broadcasts. If there is no water flowing, there is no power to the device, so indeed no data. The data should have come in within a few seconds after the water runs and the display starts showing data. It might be your bluetooth isn’t getting though correctly.

I do not know if it was autodetected or that you had to enter a BLE MAC address though. If it is autodetected I would expect it to work when the shower runs, and that bluetooth is able to see the device. If does not, they might have changes the manufacturing code somewhat, or your range is too limited. A bluetooth proxy might help then.

At some point however I moved away from the HA integration, and moved over to a version that runs entirely on the ESP and links to HA. It might have been because there was a bug at the time, I think that might have got fixed later on but I did not wait for it because the other solution worked fine for me.

I do not know where I got the code from, it was here somewhere on the forum I think. Creating an ESP firmware is not as hard as it sounds. The is no soldering involved. If you create a BLE proxy for it and adopt it to have the yaml in EspHome, you can simply adjust the yaml to add this in. Or you can create a default empty ESP with esp-idf framework, and add the below to that.

If you go the same route, this is what you need. Create a folder ā€œpackagesā€ besides the yaml of your esp. Two files will go there: First there needs to be a file present names oras_dsh.h with this in it:

#include "esphome.h"
using namespace esphome;

class DSH {
  public:

    unsigned long tStartTime;
    unsigned long tLastUpdate;

    char szLastShowerEnded[32]; // Human readable of tLastUpdate

    unsigned long lCurrentShowerNo;  // 
    unsigned long lLastShowerNo;  // No of previous shower - used to find out if previous shower is being continued

    float curWaterLiters;
    float curEnergy;
    float curBathTemp;
    uint16_t  curShowerFlowDuration;

    float lastWaterLiters=0;
    float lastEnergy=0;
    float lastBathTemp=0;
    uint16_t  lastShowerFlowDuration;

    unsigned long lastShowerDuration=0;

    bool occupancy=0;

    uint8_t bleData[20];

} dsh;

Then I broke out the shower device in a separate file shower.yaml with this in it. You also put it in the packages folder. At some places you need to replace the MAC address with the one of your showerhead:

esphome:
  devices:
    - id: showerhead
      name: Showerhead
  includes:
    - packages/oras_dsh.h   # File is included in order to manage "global" data between the different sections of lambda code

    # Time component is needed for time calculations
time:
  - platform: homeassistant
    id: homeassistant_time

text_sensor:
  - platform: template
    device_id: showerhead
    name: "dsh_lastShowerFinished"
    id: dsh_lastShowerFinished
    icon: mdi:calendar-clock
    update_interval: never

esp32_ble_tracker:
  scan_parameters:
    active: true

  on_ble_advertise:
    - then:
      - lambda: |-
          char stringbuf[256];                             // Buffer for outputting raw data in a readable format               
          char* buf2 = stringbuf;                          // Pointer for buffer
          char* endofbuf = stringbuf + sizeof(stringbuf);  // Pointer to end of buffer
          int i;                                           // integer to be sued for counter

          if(strcmp(x.get_name().c_str(),"DHS") == 0) {    // Check manufacturer name
            ESP_LOGI("DSH_tracker", "New BLE device");
            ESP_LOGI("DSH_tracker", "  address: %s", x.address_str().c_str());
            ESP_LOGI("DSH_tracker", "(%s)  name: %s", x.address_str().c_str(), x.get_name().c_str());
            ESP_LOGI("DSH_tracker", "(%s)  Advertised service UUIDs:",x.address_str().c_str());
            for (auto uuid : x.get_service_uuids()) {
                ESP_LOGI("DSH_tracker", "(%s)    - %s", x.address_str().c_str(), uuid.to_string().c_str());
            }
            ESP_LOGI("DSH", "(%s)  Advertised service data:",x.address_str().c_str());
            for (auto data : x.get_service_datas()) {
                ESP_LOGI("DSH_tracker", "(%s)    - %s: (length %i)", x.address_str().c_str(), data.uuid.to_string().c_str(), data.data.size());
            }
            ESP_LOGI("DSH_tracker", "(%s)  Advertised manufacturer data:",x.address_str().c_str());
            for (auto data : x.get_manufacturer_datas()) {
              ESP_LOGI("DSH_tracker", "(%s)    - %s: (length %i)", x.address_str().c_str(), data.uuid.to_string().c_str(), data.data.size());
              for(i=0;i<18;i++) {
                if (buf2 + 5 < endofbuf) {
                    if (i > 0) {
                        buf2 += sprintf(buf2, ":");
                    }
                  buf2 += sprintf(buf2, "%02X", data.data[i]);                  
                }
              }
              ESP_LOGI("DSH_tracker", "(%s) Data block: %s", x.address_str().c_str(), (char*)stringbuf);

              // Detailed analysis of data - check if shower head is active (only works before connection is established with ble_client)
              if ( dsh.occupancy == 0 && data.data[14]==0x32 && data.data[15]==0x4F) {   // Enhance filtering on data - to ensure correct match
                auto time = id(homeassistant_time).now();        // Get current time
                time_t currTime = time.timestamp;

                unsigned long int totShowers = data.data[2];         // Total Showers consist of index 1 and 2 in the array
                unsigned long int bathTemp =  data.data[11];         // Temperature consist of index 11 in the array
                unsigned long int bathKwhDec= data.data[13];         // kWh consumptionm consist of index 12 and 13 in the array (multiplied by 100)
                unsigned long int bathLiter =  data.data[9];         // L consumptionm consist of index 8 and 9 in the array (multiplied by 100)
                unsigned int iShowerDuration = data.data[7];         // idx 6+7 = shower time 

                dsh.tStartTime = time.timestamp;  // Set start time of shower
                ESP_LOGW("DSH_tracker", "Initializing start time to: %ld and setting occupancy on",time.timestamp);
                dsh.occupancy =1 ;

                totShowers += ( data.data[1]<<8);                   // Calculate shower number
                dsh.lCurrentShowerNo = totShowers;
                dsh.curBathTemp = bathTemp;
                bathKwhDec += ( data.data[12]<<8);                  // Get data for energy (16 bit number) - number must be divided by 100
                dsh.curEnergy = (float)bathKwhDec/100;
                bathLiter += ( data.data[8]<<8);                    // Get amount of water (16 bit number) - number must be divided by 10
                dsh.curWaterLiters = float(bathLiter)/10;
                iShowerDuration += ( data.data[6]<<8);              // Get data for shower time (16 bit number)
                dsh.curShowerFlowDuration = iShowerDuration;

                if(memcmp(&dsh.bleData[0],&data.data[0],18)!=0) {   // If BLE data is different than what we have in the "cache", copy the new data
                  memcpy(&dsh.bleData[0],&data.data[0],18);     
                }
                ESP_LOGI("DSH_tracker", "Data block changes since last: %s", (char*)stringbuf);
                dsh.tLastUpdate = time.timestamp;                   // Set information about when data was last updated
                ESP_LOGI("DSH_tracker", "Timestamp for last update set to: %ld",time.timestamp);
                strftime(dsh.szLastShowerEnded, sizeof(dsh.szLastShowerEnded), "%Y-%m-%d %H:%M:%S", localtime(&currTime));
              }
            }
          }
          
# Timer running  on 5 sec interval - to ensure fairly quickly updates once the shower is active
interval:
  - interval: 5sec
    then:
      - lambda: |-
          // Get current time
          auto time = id(homeassistant_time).now();
          // ESP_LOGW("DSH", "Interval timer spawned");
          if ( (time.timestamp - dsh.tLastUpdate) > 180 && dsh.tLastUpdate != 0 ) {   // More than 180 seconds has passed since last update, assume finished
            unsigned long int showerDurationSecs = dsh.tLastUpdate - dsh.tStartTime;  // Calculate shower duration
            dsh.occupancy = 0;  // Set variable for shower as inactive
            ESP_LOGI("DSH", "3 minutes has passed since last updated - shower duration in seconds has been calculated to: %ld seconds",showerDurationSecs);
            // Publish LastShower sensors to HA
            id(dsh_lastShowerkWh).publish_state(dsh.curEnergy); 
            id(dsh_lastShowerTemp).publish_state(dsh.curBathTemp);
            id(dsh_lastShowerLiter).publish_state(dsh.curWaterLiters);
            id(dsh_lastShowerM3).publish_state((float)dsh.curWaterLiters*0.001);
            id(dsh_lastDurationSeconds).publish_state(showerDurationSecs);
            id(dsh_showerOccupied).publish_state(dsh.occupancy);
            id(dsh_lastShowerFinished).publish_state(dsh.szLastShowerEnded);
            id(dsh_lastShowerWaterFlowTime).publish_state(dsh.curShowerFlowDuration);

            // Update internal variables for historical lookup
            dsh.lastWaterLiters=dsh.curWaterLiters;
            dsh.lastEnergy=dsh.curEnergy;
            dsh.lastBathTemp=dsh.curBathTemp;
            dsh.lastShowerDuration=showerDurationSecs;
            dsh.lastShowerFlowDuration = dsh.curShowerFlowDuration;
            // Set CurrentShower to NaN - as shower is inactive
            id(dsh_curShowerkWh).publish_state(NAN); 
            id(dsh_curShowerTemp).publish_state(NAN);
            id(dsh_curShowerLiter).publish_state(NAN);
            id(dsh_curShowerM3).publish_state(NAN);
            id(dsh_curShowerTimeSeconds).publish_state(NAN);
            id(dsh_curShowerWaterFlowTime).publish_state(NAN);

            // Reset internal counters and data
            ESP_LOGI("DSH", "Consider shower to be finished and reset Occupancy");
            dsh.tLastUpdate = 0;
            dsh.lLastShowerNo = dsh.lCurrentShowerNo;  // Set counter shower number - to be able to identify if current shower continues
          } else if ( dsh.tStartTime != 0 && dsh.occupancy==1 && dsh.tLastUpdate != 0) { 
            // Regular updates every 5 seconds when shower is on
            // Only send if more than 10 seconds since last update from BLE device
            unsigned long int showerDurationSecs = dsh.tLastUpdate - dsh.tStartTime;  // Calculate shower duration
            ESP_LOGW("DSH", "Updating HA sensors ... ");
            id(dsh_curShowerkWh).publish_state(dsh.curEnergy);
            id(dsh_curShowerTemp).publish_state(dsh.curBathTemp);
            id(dsh_curShowerLiter).publish_state(dsh.curWaterLiters);
            id(dsh_curShowerM3).publish_state((float)dsh.curWaterLiters*0.001);
            id(dsh_curShowerTimeSeconds).publish_state(showerDurationSecs);
            id(dsh_showerOccupied).publish_state(dsh.occupancy);
            id(dsh_totShowers).publish_state(dsh.lCurrentShowerNo);
            id(dsh_lastShowerWaterFlowTime).publish_state(dsh.curShowerFlowDuration);
          }

ble_client:
  - mac_address: D8:71:4D:C3:FB:50 # Address needs to match shower head - 60:77:71:3A:D6:BB
    id: DSH
    on_connect:
      then:
        lambda: |- 
          auto time = id(homeassistant_time).now();
          ESP_LOGI("DSH", "Shower head connected ...");

    on_disconnect:
      then:
        lambda: |- 
          // In principle a disconnect only means that the shower head disabled BLE - it does not mean, that the shower is finished,
          // as there is a 2 minute timeout in the shower head
          auto time = id(homeassistant_time).now();
          ESP_LOGI("DSH", "Shower head disconnected - if no new data has been received after approx 3 minutes, the shower is considered finished");

# Sensor to indicate whether the shower is busy/occupied or not.
binary_sensor:
  - platform: template
    device_id: showerhead
    name: "dsh_showerOccupied"
    id: dsh_showerOccupied
    icon: 'mdi:shower-head'
    device_class: occupancy

sensor:
  # Total number of showers registered by the shower-head 
  - platform: template
    device_id: showerhead
    name: "dsh_totShowers"
    id: dsh_totShowers
    icon: 'mdi:speedometer-medium'
    unit_of_measurement: "x"
    update_interval: never

  # Current temperature
  - platform: template
    device_id: showerhead
    name: "dsh_curShowerTemp"
    id: dsh_curShowerTemp
    icon: 'mdi:thermometer'
    device_class: temperature
    update_interval: never
    unit_of_measurement: "°C"

  # Previous temperature
  - platform: template
    device_id: showerhead
    name: "dsh_lastShowerTemp"
    id: dsh_lastShowerTemp
    icon: 'mdi:thermometer'
    device_class: temperature
    update_interval: never
    unit_of_measurement: "°C"

  # Current energy usage in kwH
  - platform: template
    device_id: showerhead
    name: "dsh_curShowerkWh"
    id: dsh_curShowerkWh
    device_class: energy
    icon: 'mdi:water-thermometer'
    accuracy_decimals: 2
    unit_of_measurement: "kWh"
    update_interval: never
    state_class: total_increasing

  # Previous energy usage in kwH
  - platform: template
    device_id: showerhead
    name: "dsh_lastShowerkWh"
    id: dsh_lastShowerkWh
    device_class: energy
    icon: 'mdi:water-thermometer'
    accuracy_decimals: 2
    unit_of_measurement: "kWh"
    update_interval: never
    state_class: total_increasing

  # Current duration 
  - platform: template
    device_id: showerhead
    name: "dsh_curShowerTimeSeconds"
    id: dsh_curShowerTimeSeconds
    icon: 'mdi:timer'
    unit_of_measurement: "s"
    update_interval: never
    state_class: total_increasing

  # Last duration 
  - platform: template
    device_id: showerhead
    name: "dsh_lastDurationSeconds"
    id: dsh_lastDurationSeconds
    icon: 'mdi:timer'
    unit_of_measurement: "s"
    update_interval: never
    state_class: total_increasing

  # Duration timer from shower head
  - platform: template
    device_id: showerhead
    name: "dsh_curShowerWaterFlowTime"
    id: dsh_curShowerWaterFlowTime
    icon: 'mdi:timer'
    unit_of_measurement: "s"
    update_interval: never
    state_class: total_increasing

  # Last duration timer from shower head
  - platform: template
    device_id: showerhead
    name: "dsh_lastShowerWaterFlowTime"
    id: dsh_lastShowerWaterFlowTime
    icon: 'mdi:timer'
    unit_of_measurement: "s"
    update_interval: never
    state_class: total_increasing

  # Current water consumption in Liter
  - platform: template
    device_id: showerhead
    name: "dsh_curShowerLiter"
    id: dsh_curShowerLiter
    device_class: water
    icon: 'mdi:water'
    unit_of_measurement: "L"
    accuracy_decimals: 1
    update_interval: never
    state_class: total_increasing

  # Last water consumption in Liter
  - platform: template
    device_id: showerhead
    name: "dsh_lastShowerLiter"
    id: dsh_lastShowerLiter
    device_class: water
    icon: 'mdi:water'
    unit_of_measurement: "L"
    accuracy_decimals: 1
    update_interval: never
    state_class: total_increasing

  # Current water consumption in m³
  - platform: template
    device_id: showerhead
    name: "dsh_curShowerM3"
    id: dsh_curShowerM3
    device_class: water
    icon: 'mdi:cup-water'
    unit_of_measurement: "m³"
    accuracy_decimals: 5
    update_interval: never
    state_class: total_increasing

  # Previous water consumption in m³
  - platform: template
    device_id: showerhead
    name: "dsh_lastShowerM3"
    id: dsh_lastShowerM3
    device_class: water
    icon: 'mdi:cup-water'
    unit_of_measurement: "m³"
    accuracy_decimals: 5
    update_interval: never
    state_class: total_increasing

  # Total number of showers - this is where the magic happens
  - platform: ble_client
    device_id: showerhead
    name: "DSH Total Showers"
    ble_client_id: DSH
    id: dsh_totalShowers
    update_interval: never
    internal: true
    type: characteristic
    service_uuid: '7f402200-504f-4c41-5261-6d706869726f'        # Not sure if these are the same on all shower heads - as I only have a single showerhead
    characteristic_uuid: '7f402203-504f-4c41-5261-6d706869726f' #
    notify: true  # Enable notifications on the service/charateristic
    lambda: |-
      auto time = id(homeassistant_time).now();        // Get current time
      time_t currTime = time.timestamp;                // Struct to generate human readable date/timestamp
      char stringbuf[256];                             // Buffer for outputting raw data in a readable format               
      char* buf2 = stringbuf;                          // Pointer for buffer
      char* endofbuf = stringbuf + sizeof(stringbuf);  // Pointer to end of buffer
      int i;                                           // integer to be sued for counter
      uint8_t* pdata = (uint8_t*) x.data();            // The BLE data array
      unsigned long int totShowers = pdata[2];         // Total Showers consist of index 1 and 2 in the array
      unsigned long int bathTemp =  pdata[11];         // Temperature consist of index 11 in the array
      unsigned long int bathKwhDec= pdata[13];         // kWh consumptionm consist of index 12 and 13 in the array (multiplied by 100)
      unsigned long int bathLiter =  pdata[9];         // L consumptionm consist of index 8 and 9 in the array (multiplied by 10)
      unsigned int iShowerFlowDuration = pdata[7];         // idx 6+7 = shower time 

      totShowers += ( pdata[1]<<8);
      // ESP_LOGD("ble_adv", "Total showers %ld", totShowers);
      dsh.lCurrentShowerNo = totShowers;

      // Only reset counters when a new shower being started - as it can have paused. This can be identified by the shower number 
      // retrieved from the BLE data
      if(dsh.occupancy == 0 && dsh.lLastShowerNo != dsh.lCurrentShowerNo) { 
        dsh.tStartTime = time.timestamp;  // Set start time of shower
        ESP_LOGW("DSH", "Initializing start time to: %ld and setting occupancy on",time.timestamp);
        id(dsh_curShowerkWh).publish_state(0);
        id(dsh_curShowerTemp).publish_state(0);
        id(dsh_curShowerLiter).publish_state(0);
        id(dsh_curShowerM3).publish_state(0);
        id(dsh_curShowerTimeSeconds).publish_state(0);
        id(dsh_curShowerWaterFlowTime).publish_state(0);
        memset(&dsh.bleData[0],0,sizeof(dsh.bleData));        // Zeroize data block buffer
        dsh.occupancy = 1;                                    // Set shower as active/occupied
        id(dsh_showerOccupied).publish_state(dsh.occupancy);  // Publish sensor to HA
      }

      dsh.curBathTemp = bathTemp;
      // ESP_LOGD("ble_adv", "Bath temperature %ld", dsh.curBathTemp);

      bathKwhDec += ( pdata[12]<<8);
      dsh.curEnergy = (float)bathKwhDec/100;
      // ESP_LOGD("ble_adv", "Bath Energy %f kWh", dsh.curEnergy);

      bathLiter += ( pdata[8]<<8);
      dsh.curWaterLiters = float(bathLiter)/10;
      // ESP_LOGD("ble_adv", "Bath water consumption %f L", dsh.curWaterLiters);

      iShowerFlowDuration += ( pdata[6]<<8);              // Get data for shower time (16 bit number)
      dsh.curShowerFlowDuration = iShowerFlowDuration;

      // Find out if data has changed compared to previous notification
      if(memcmp(&dsh.bleData[0],pdata,18)!=0) {
        // Data is different, copy new data
        memcpy(&dsh.bleData[0],pdata,18);
        // Dump hex data to the log
        for(i=0;i<18;i++) {
          if (buf2 + 5 < endofbuf) {
              if (i > 0) {
                  buf2 += sprintf(buf2, ":");
              }
            buf2 += sprintf(buf2, "%02X", pdata[i]);                  
          }
        }
        ESP_LOGI("DSH", "Data block changes since last: %s", (char*)stringbuf);
        // Set information about when data was last updated
        dsh.tLastUpdate = time.timestamp;
        ESP_LOGI("DSH", "Timestamp for last update set to: %ld",time.timestamp);
        strftime(dsh.szLastShowerEnded, sizeof(dsh.szLastShowerEnded), "%Y-%m-%d %H:%M:%S", localtime(&currTime));
        // sprintf((char *)dsh.szLastShowerEnded, "%04d-%02d-%02d %02d:%02d:%02d", time.year,time.month,time.day_of_month,time.hour,time.minute,time.second);
      }
      return(totShowers);

And finally, you need to add this to the yaml of your esp:

packages:
  showerhead: !include packages/shower.yaml

This has been working for me ever since, no problems. The advantage is the BLE device can be placed near the shower.

If the above also does not work, you can very likely use the ESP logging in this code to see why it isn’t recognised and maybe adjust the manufacturer code if that is the problem.

I have one of those and it’s detected by the Theengs Gateway Addon

These are the sensors, they show unknown when the shower is off. The daily volume is a utility meter I set up