Lovelace input value

Hi,

I have linked a grainfather device via BLE to esp32 running esphome.

I am reading data from the device and sending pushbutton commands back to the device but I need to also send numbers and text. How can I create a card on Lovelace that will allow a user to input a value or text?

I assume I need to use a helper to capture the value but I’d prefer not to just add the helper entity as a card as it’s not an aesthetically pleasing user interface to input/update a value.

Numbers would be between 1 and 100, text would be short, max 20 characters.

(For example if the device is sending back that a timer has 5 mins remaining, I want to be able to update/amend that to 10 mins for example)

Thanks in advance, I’ve spent hours and hours trying to work out this last piece.

1 Like

For the number just create this in esphome.

It will appear in Home Assistant so you can add it to a dashboard card.

For the text you will have to create an input_text helper in home assistant and use this to bring it into esphome:

Thanks. I’ll give that a go tomorrow.

Unfortunately didn’t achieve what I was after. Reacted the same as a Number helper. For the temperature control portion I’m making good progress using a thermostat object even though that’s not what it is, the principles are the same. Once I have that sorted I’ll have another go at the Text Input. (If anyone has any ideas, that’d be great. I basically want to put a card on Lovelace that you type directly into - on enter, I will write it to both a helper and send it back to the BLE controller on ESPHome).

Are you able to share some details of your project? I’m an avid Grainfather brewer and it would be awesome to integrate it into HA. It would allow better notifications and range.

Really interested to see what you have been able to achieve with the ESP32 and ESPHome…

Hey Gareth,
Still a work in progress. Just don’t find the time. :->

Here’s my dashboard and ESPHome config.
The basics work to replicate the Grainfather control panel to HASS, but I havent yet solved setting timers and temp using the thermostat.

Hope it helps and let me know if you get further with any of it.

views:
  - title: Home
    cards:
      - type: vertical-stack
        cards:
          - type: vertical-stack
            cards:
              - type: horizontal-stack
                cards:
                  - type: entity
                    entity: sensor.gf_current_temperature
                    name: Current
                    icon: mdi:temperature-celsius
              - type: horizontal-stack
                cards:
                  - type: entity
                    entity: sensor.gf_set_temperature
                    name: Target
                    icon: mdi:temperature-celsius
                  - show_name: false
                    show_icon: false
                    show_state: true
                    type: glance
                    entities:
                      - entity: sensor.gf_timer_minutes
                        tap_action:
                          - action: call-service
                            service: input_number.reload
                            data:
                              entity_id: input_number.gf_timer_start
                      - entity: sensor.gf_timer_seconds
                        tap_action:
                          action: none
                    title: Timer
                    tap_action:
                      action: none
          - type: horizontal-stack
            cards:
              - show_name: true
                show_icon: true
                type: button
                tap_action:
                  action: toggle
                entity: switch.pump_status
                name: Pump
              - type: vertical-stack
                cards:
                  - show_name: false
                    show_icon: true
                    type: button
                    tap_action:
                      action: toggle
                    entity: button.up
                  - show_name: false
                    show_icon: true
                    type: button
                    tap_action:
                      action: toggle
                    entity: button.set
                    icon: ''
                  - show_name: false
                    show_icon: true
                    type: button
                    tap_action:
                      action: toggle
                    entity: button.down
              - show_name: true
                show_icon: true
                type: button
                tap_action:
                  action: toggle
                entity: switch.heat_power
                name: Heat
      - type: thermostat
        entity: climate.gf_thermostat
title: Grainfather

and


external_components:
  - source:
      type: git
      url: https://github.com/camsaway/esphome-temp
      ref: ble_write_action
    components: [ ble_client ]

esphome:
  name: grainfatherwifi

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:
#  level: VERBOSE

# Enable Home Assistant API
api:
  password: !secret api_password
  encryption:
    key: uvLVRYH+M2rsmc2zjg0e06Y8OLX5NevjgVSi5FwMkFg=

ota:
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
#  manual_ip:     #Optional Manual IP
#    static_ip: 192.168.1.84
#    gateway: 192.168.1.250
#    subnet: 255.255.255.0
  ap:
    ssid: "Grainfatherwifi Fallback Hotspot"
    password: "7fz6PVTCmEBm"

captive_portal:
    
esp32_ble_tracker:

ble_client:
  - mac_address: BB:A0:50:11:29:09  #My Personal GF MAC Address
    id: gf_ble

sensor:
  - platform: template
    name: "GF Current Temperature"
    id: gf_current_temperature
    unit_of_measurement: "°C"
  - platform: template
    name: "GF Set Temperature"
    id: gf_set_temperature
    unit_of_measurement: "°C"
  - platform: template
    name: "GF Timer Minutes"
    id: gf_timerminutes
    unit_of_measurement: "min"
  - platform: template
    name: "GF Timer Seconds"
    id: gf_timerseconds
    unit_of_measurement: "s"
switch:
  - platform: template
    name: "Pump Status"
    id: gf_pump
    icon: "mdi:pump"
    turn_on_action:
      - ble_client.ble_write:
          id: gf_ble
          service_uuid: 0000cdd0-0000-1000-8000-00805f9b34fb
          characteristic_uuid: 0003cdd2-0000-1000-8000-00805f9b0131
          value: [0x4c, 0x31, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20]
    turn_off_action:
      - ble_client.ble_write:
          id: gf_ble
          service_uuid: 0000cdd0-0000-1000-8000-00805f9b34fb
          characteristic_uuid: 0003cdd2-0000-1000-8000-00805f9b0131
          value: [0x4c, 0x30, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20]
  - platform: template
    name: "Heat Power"
    id: gf_heat
    icon: "mdi:heat-wave"
    turn_on_action:
      - ble_client.ble_write:
          id: gf_ble
          service_uuid: 0000cdd0-0000-1000-8000-00805f9b34fb
          characteristic_uuid: 0003cdd2-0000-1000-8000-00805f9b0131
          value: [0x4b, 0x31, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20]
    turn_off_action:
      - ble_client.ble_write:
          id: gf_ble
          service_uuid: 0000cdd0-0000-1000-8000-00805f9b34fb
          characteristic_uuid: 0003cdd2-0000-1000-8000-00805f9b0131
          value: [0x4b, 0x30, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20]
button:
  - platform: template
    name: "Up"
    id: gf_up_button
    icon: "mdi:arrow-up-drop-circle"
    on_press:
      - ble_client.ble_write:
          id: gf_ble
          service_uuid: 0000cdd0-0000-1000-8000-00805f9b34fb
          characteristic_uuid: 0003cdd2-0000-1000-8000-00805f9b0131
          value: [0x55, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20]    
  - platform: template
    name: "Down"
    id: gf_down_button
    icon: "mdi:arrow-down-drop-circle"
    on_press:
      - ble_client.ble_write:
          id: gf_ble
          service_uuid: 0000cdd0-0000-1000-8000-00805f9b34fb
          characteristic_uuid: 0003cdd2-0000-1000-8000-00805f9b0131
          value: [0x44, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20]    
  - platform: template
    name: "Set"
    id: gf_set_button
    icon: "mdi:alpha-s-circle-outline"
    on_press:
      - ble_client.ble_write:
          id: gf_ble
          service_uuid: 0000cdd0-0000-1000-8000-00805f9b34fb
          characteristic_uuid: 0003cdd2-0000-1000-8000-00805f9b0131
          value: [0x54, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20]    

text_sensor:
  - platform: ble_client
    ble_client_id: gf_ble
    name: "GF Data"
    icon: mdi:database-check-outline
    service_uuid: '0000cdd0-0000-1000-8000-00805f9b34fb'
    characteristic_uuid: '0003cdd1-0000-1000-8000-00805f9b0131'
    notify: true
    update_interval: 2s
    internal: true
    filters:
      - lambda: |-
          std::basic_string<char> retstr;
          size_t delimiter;
          float value;
          retstr = x.substr(1,x.length());
          if (x[0] == 'X') {
            delimiter = retstr.find(',');
            value = atof(retstr.substr(0,delimiter).c_str());
            auto call = id(gf_thermostat).make_call();
            call.set_target_temperature(value);
            call.perform();
            id(gf_set_temperature).publish_state(value);
            retstr = retstr.substr(delimiter+1,retstr.length());
            delimiter = retstr.find(',');
            value = atof(retstr.substr(0,delimiter).c_str());
            id(gf_current_temperature).publish_state(value);
            retstr = retstr.substr(delimiter+1,retstr.length());
          }
          else if (x[0] == 'Y') {
            delimiter = retstr.find(',');
            value = atof(retstr.substr(0,delimiter).c_str());
            id(gf_heat).publish_state(value);
            retstr = retstr.substr(delimiter+1,retstr.length());
            delimiter = retstr.find(',');
            value = atof(retstr.substr(0,delimiter).c_str());
            id(gf_pump).publish_state(value);
            retstr = retstr.substr(delimiter+1,retstr.length());
          }
          /*
          else if (x[0] == 'W') {
            delimiter = retstr.find(',');
            value = atof(retstr.substr(0,delimiter).c_str());
            id(gf_power).publish_state(value);
            retstr = retstr.substr(delimiter+1,retstr.length());
          }
          else if (x[0] == 'V') {
            delimiter = retstr.find(',');
            value = atof(retstr.substr(0,delimiter).c_str());
            id(gf_voltage).publish_state(value);
            retstr = retstr.substr(delimiter+1,retstr.length());
            delimiter = retstr.find(',');
            id(gf_temp_units).publish_state(retstr.substr(0,delimiter).c_str());
            retstr = retstr.substr(delimiter+1,retstr.length());
          }
          else if (x[0] == 'C') {
            delimiter = retstr.find(',');
            value = atof(retstr.substr(0,delimiter).c_str());
            id(gf_boil_temperature).publish_state(value);
            retstr = retstr.substr(delimiter+1,retstr.length());
          }
          else if (x[0] == 'F') {
            delimiter = retstr.find(',');
            id(gf_version).publish_state(retstr.substr(0,delimiter).c_str());
            retstr = retstr.substr(delimiter+1,retstr.length());
          }
          */
          else if (x[0] == 'T') {
            delimiter = retstr.find(',');
            value = atof(retstr.substr(0,delimiter).c_str());
            retstr = retstr.substr(delimiter+1,retstr.length());
            delimiter = retstr.find(',');
            value = atof(retstr.substr(0,delimiter).c_str());
            id(gf_timerminutes).publish_state(value);
            retstr = retstr.substr(delimiter+1,retstr.length());
            delimiter = retstr.find(',');
            value = atof(retstr.substr(0,delimiter).c_str());
            retstr = retstr.substr(delimiter+1,retstr.length());
            delimiter = retstr.find(',');
            value = atof(retstr.substr(0,delimiter).c_str());
            id(gf_timerseconds).publish_state(value);
            retstr = retstr.substr(delimiter+1,retstr.length());
          }
          /*
          else if (x[0] == 'I') {
            //Interaction Mode
            retstr = 'I';
          }
          else if (x[0] == 'A') {
            retstr = 'A';
          }
          else if (x[0] == 'E') {
            retstr = 'E';
          }
          else if (x[0] == 'B') {
            retstr = 'B';
          }
          */
          else {
            retstr = x;
          }
          return retstr;

climate:
  - platform: thermostat
    icon: mdi:thermostat
    name: "GF Thermostat"
    id: gf_thermostat
    sensor: gf_current_temperature
    default_target_temperature_low: 60
    min_idle_time: 0s
    idle_action:
      - delay: 0s
    heat_action:
      - delay: 0s
    min_heating_off_time: 0s
    min_heating_run_time: 0s
    visual:
      min_temperature: 0
      max_temperature: 100
      temperature_step: 1
    target_temperature_change_action:
      then:
        - lambda: |-
            int target = id(gf_thermostat).target_temperature;
            std::vector<unsigned char> GFcommand;
            //GFcommand = {0x24, 0x35, 0x30, 0x2c, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20};
            if (target > 99) {
                ESP_LOGD("GFCode", "Writing 120C");
                GFcommand = {0x24, 0x31, 0x32, 0x30, 0x2c, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20};
            }
            else if (target < 10) {
                ESP_LOGD("GFCode", "Writing 0C");
                GFcommand = {0x24, 0x30, 0x2c, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20};
            }
            else {
                ESP_LOGD("GFCode", "Writing some value");
                GFcommand[0] = 0x24;
                switch((target/10)%10){
                    case 0: GFcommand[1] = 0x30;
                    case 1: GFcommand[1] = 0x31;
                    case 2: GFcommand[1] = 0x32;
                    case 3: GFcommand[1] = 0x33;
                    case 4: GFcommand[1] = 0x34;
                    case 5: GFcommand[1] = 0x35;
                    case 6: GFcommand[1] = 0x36;
                    case 7: GFcommand[1] = 0x37;
                    case 8: GFcommand[1] = 0x38;
                    case 9: GFcommand[1] = 0x39;
                    default: GFcommand[1] = 0x20;
                }
                switch(target % 10){
                    case 0: GFcommand[2] = 0x30;
                    case 1: GFcommand[2] = 0x31;
                    case 2: GFcommand[2] = 0x32;
                    case 3: GFcommand[2] = 0x33;
                    case 4: GFcommand[2] = 0x34;
                    case 5: GFcommand[2] = 0x35;
                    case 6: GFcommand[2] = 0x36;
                    case 7: GFcommand[2] = 0x37;
                    case 8: GFcommand[2] = 0x38;
                    case 9: GFcommand[2] = 0x39;
                    default: GFcommand[2] = 0x20;
                }
                GFcommand[3] = 0x2c;
                for (int i = 4; i < 19; i++) {
                    GFcommand[i] = 0x20;
                }
            }
            ble_client::BLEClientWriteAction<> *my_bleclientwriteaction_1;
            my_bleclientwriteaction_1 = new ble_client::BLEClientWriteAction<>(gf_ble);
            my_bleclientwriteaction_1->set_value(GFcommand);
            my_bleclientwriteaction_1->set_service_uuid128((uint8_t*)(const uint8_t[16]){0xFB,0x34,0x9B,0x5F,0x80,0x00,0x00,0x80,0x00,0x10,0x00,0x00,0xD0,0xCD,0x00,0x00});
            my_bleclientwriteaction_1->set_char_uuid128((uint8_t*)(const uint8_t[16]){0x31,0x01,0x9B,0x5F,0x80,0x00,0x00,0x80,0x00,0x10,0x00,0x00,0xD2,0xCD,0x03,0x00});
            my_bleclientwriteaction_1->write();
            
# HEX CONVERSIONS

# ABCDEFGHIJKLMNOPQRSTUVWXYZ
# 0123456789
# $,  (space)
# abcdefghijklmnopqrstuvwxyz

# 0x41 0x42 0x43 0x44 0x45 0x46 0x47 0x48 0x49 0x4a 0x4b 0x4c 0x4d 0x4e 0x4f 0x50 0x51 0x52 0x53 0x54 0x55 0x56 0x57 0x58 0x59 0x5a
# 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39
# 0x24 0x2c 0x20
# 0x61 0x62 0x63 0x64 0x65 0x66 0x67 0x68 0x69 0x6a 0x6b 0x6c 0x6d 0x6e 0x6f 0x70 0x71 0x72 0x73 0x74 0x75 0x76 0x77 0x78 0x79 0x7a


# ################################################################
# # https://github.com/kingpulsar/Grainfather-Bluetooth-Protocol #
# ################################################################

# ==========================
# Bluetooth Low Energy
# ==========================
# The system uses Bluetooth Low Energy for it's communiation.
# Here's a list with the UUIDs for the grainfather system.

# Device UUID: 0000cdd0-0000-1000-8000-00805f9b34fb
# Read Characteristic UUID: 0003cdd1-0000-1000-8000-00805f9b0131
# Write Characteristic UUID 0003cdd2-0000-1000-8000-00805f9b0131

# ==============
# Commands
# ==============
# The commands are sent to the grainfather system via Bluetooth. Commands are plain text.
# All commands should be exactly 19 characters in length. Pad the end with spaces.
# Anything between curly braces is a parameter.

# Dismiss Boil Addition Alert: A
# Cancel Timer: C
# Decrement Target Temp: D
# Cancel Or Finish Session: F
# Pause Or Resume Timer: G
# Toggle Heat: H
# Interaction Complete: I
# Turn Off Heat: K0
# Turn On Heat: K1
# Turn Off Pump: L0
# Turn On Pump: L1
# Get Current Boil Temp: M
# Toggle Pump: P
# Disconnect Manual Mode No Action: Q0
# Disconnect And Cancel Session: Q1
# Disconnect Auto Mode No Action: Q2
# Press Set: T
# Increment Target Temp: U
# Disable Sparge Water Alert: V
# Get Firmware Version: X
# Reset Controller: Z
# Reset Recipe Interrupted: !
# Turn Off Sparge Counter Mode: d0
# Turn On Sparge Counter Mode: d1
# Turn Off Boil Control Mode: e0
# Turn On Boil Control Mode: e1
# Exit Manual Power Control Mode: f0
# Enter Manual Power Control Mode: f1
# Get Controller Voltage And Units: g
# Turn Off Sparge Alert Mode: h0
# Turn On Sparge Alert Mode: h1
# Set Delayed Heat Function: B{Minutes},{Seconds},
# Set Local Boil Temp To: E{Temperature},
# Set Boil Time To: J{Minutes},
# Skip To Step: N{Step Num},{Can Edit Time},{Time Left Minutes},{Time Left Seconds},{Skip Ramp},{Disable Add Grain},
# Set New Timer: S{Minutes},
# Set New Timer With Seconds: W{Minutes},{Seconds},
# Set Target Temp To: ${Temperature},
# Edit Controller Stored Temp And Time: a{Stage Num},{New Time},{New Temperature},
# Set Sparge Progress To: b${Progress},
# Skip To Interaction: c{Code},

# ======================
# Sending a recipe
# ======================
# Sending a recipe is a bit more complex. Commands need to be sent in the correct order:
# R{Boil Time},{Mash Step Count},{Mash Volume},{Sparge Volume},
# {Show Water Treatment Alert},{Show Sparge Counter},{Show Sparge Alert},{Delayed Session},{Skip Start},
# {Recipe Name}
# {Hop Stand Time},{Boil Addition Stop Count},{Boil Power Mode},{Strike Temp Mode},
# Then for every 'boil stop' or 'unique boil addition time', send the time remaining in minutes.
# Then if you're using strike temp mode, send: 0. (Not sure why, maybe it's not yet implemented)
# Then for every 'mash stop' or 'step' in your mash, send: {Mash Step Temperature}:{Mash Step Duration} (Temperature in degC and Duration in Min)
# If delayed session is enabled, send: {Delay Minutes}, {Delay Seconds}

# Example:
# R75,2,15.7,16.7, 75 minute boil, 2 mash steps, 15.6L mash volume, 16.7L sparge volume
# 0,1,1,0,0, No water treatment alert, show sparge counter, show sparge alert, no delayed session, do not skip the start
# SAISON Recipe name that will be displayed in the top left
# 0,4,0,0, No hop stand, 4 boil addition stops, no boil power mode, no strike temp mode
# 75, Boil addition stop 1, at 75 minutes boil time remaining
# 45, Boil addition stop 2, at 45 minutes boil time remaining
# 30, Boil addition stop 3, at 30 minutes boil time remaining
# 10, Boil addition stop 4, at 10 Minutes boil time remaining
# 65:60, Mash step 1, 65C for 60 minutes
# 75:10, Mash step 2, 75C for 10 minutes

# =====================
# Notifications
# =====================
# The system sends it's current status every so often.
# All notifications start with one of these characters: A, B, C, E, F, I, T, V, W, X, Y.
# Notifications look like this:

# X{Target Temperature},{Current temperature}
# Y{Heat Power},{Pump Status},{Auto Mode Status},{Stage Ramp Status},{Interaction Mode Status},{Interaction Code},{Stage Number},{Delayed Heat Mode}
# T{Timer Active},{Time Left Minutes},{Timer Total Start Time},{Time Left Seconds}
# I{Interaction Code}
# V{Voltage: 0=230V, 1=110V},{Units: 0=°F, 1=°C}
# W{Heat Power Output Percentage},{Is Timer Paused},{Step Mash Mode},{Is Recipe Interrupted},{Manual Power Mode},{Sparge Water Alert Displayed}
# C{Boil Temperature}
# F{Firmware version}

# Examples:
# T0,0,0,0,ZZZZZZZZ
# X61.0,22.8,ZZZZZZ 61.0C target temperature, 22.8C current temperature
# W0,0,0,0,0,0,ZZZZ

1 Like

Thanks so much for sharing. This is excellent and I’m looking forward to getting it integrated!