Pi Zero + Rotary Encoder as Hass Volume Controller & Play-Pause Toggle (no soldering required)

Simple little thing, but I love it.

And here it is again, a bit more dressed for the occasion.

*Edit: Updated code to make use of the Push Button on the Encoder as a Play / Pause toggle for a media player. Involves one extra wire (the white wire in the photo) in addition to the wiring guide below. All other instructions the same though. I am getting some false double-pushes every now and again, code might need some tweaking.

The rotary encoder was one of the pieces in an electronics kit I got from China. All fairly standard I guess.

The Pi Zero is just Raspbian Lite, with Home Assistant installed for the python api module.

I followed the wiring guide here:

https://www.modmypi.com/blog/how-to-use-a-rotary-encoder-with-the-raspberry-pi

Code:

http://www.14core.com/wiring-the-360-rotary-encoder-on-c-code-python/

Re-used the code, added some hass specific bits, and now control my stereo with it :slight_smile:

Here are the code bits:

First an input_number.yaml entry to hold the rotary value, it won’t be for display:

rotary_encoder_value:
  name: Rotary
  min: 0
  max: 100
  step: 1
  mode: box

And some sensor.yaml entries:

  - platform: template
    sensors:
      rotary_value:
        value_template: "{{ states.input_number.rotary_encoder_value.state }}"


  - platform: template
    sensors:
      hifipi_volume:
        value_template: "{{ states.media_player.hifi_pi.attributes.volume_level * 100}}"

Change the “media_player.hifi_pi” part to match your player, and in the bit below.

The automation.yaml entry to change the volume when the encoder’s turned:

- id: Adjust Volume on Rotary Encoder Move
  alias: "Rotary Volume Change"
  trigger:
    - platform: state
      entity_id: sensor.rotary_value
  action:
    - service: media_player.volume_set
      entity_id: media_player.hifi_pi
      data_template:
        volume_level: "{{ states.sensor.rotary_value.state | float / 100 }}"

For the Pi Zero itself, run this script. It’s python3. Change the api IP address to match your Hass install’s, and the “switch_name” to your media_player.

#!/usr/bin/env python3
import RPi.GPIO as GPIO
import time
import homeassistant.remote as remote

api = remote.API('192.168.178.113', '')
domain = 'media_player'
switch_name = 'media_player.hifi_pi'

media_link = remote.get_state(api, 'sensor.hifipi_volume')

if media_link.state == "unknown": media_link.state = "40"

counter = int(float(media_link.state))

remote.set_state(api, 'sensor.rotary_encoder_value', new_state=counter)

# print("ok ", counter)

RoAPin = 11    # pin11
RoBPin = 12    # pin12
RoSPin = 13    # pin13

globalCounter = counter

flag = 0
Last_RoB_Status = 0
Current_RoB_Status = 0

def setup():
	GPIO.setmode(GPIO.BOARD)       # Numbers GPIOs by physical location
	GPIO.setup(RoAPin, GPIO.IN)    # input mode
	GPIO.setup(RoBPin, GPIO.IN)
	GPIO.setup(RoSPin,GPIO.IN,pull_up_down=GPIO.PUD_UP)
	rotaryButton()

def rotary_counter_return_value(ro_count):
  remote.set_state(api, 'input_number.rotary_encoder_value', new_state=ro_count)
  return()

def rotaryButton():
        GPIO.add_event_detect(RoSPin, GPIO.FALLING, callback=button_press) # wait for falling

def button_press(ev=None):
        remote.call_service(api, domain, 'toggle', {'entity_id': '{}'.format(switch_name)}, timeout=7)
        time.sleep(1)

def rotaryDeal():
	global flag
	global Last_RoB_Status
	global Current_RoB_Status
	global globalCounter
	Last_RoB_Status = GPIO.input(RoBPin)
	while(not GPIO.input(RoAPin)):
		Current_RoB_Status = GPIO.input(RoBPin)
		flag = 1
	if flag == 1:
		flag = 0
		if (Last_RoB_Status == 0) and (Current_RoB_Status == 1):
			globalCounter = globalCounter + 2
			rotary_counter_return_value(globalCounter)
		if (Last_RoB_Status == 1) and (Current_RoB_Status == 0):
			globalCounter = globalCounter - 2
			rotary_counter_return_value(globalCounter)

def loop():
	global globalCounter
	while True:
		rotaryDeal()

def destroy():
	GPIO.cleanup()             # Release resource

if __name__ == '__main__':     # Program start from here
	setup()
	try:
		loop()
	except KeyboardInterrupt:  # When 'Ctrl+C' is pressed, the child program destroy() will be  executed.
		destroy()

A group.yaml

Rotary:
  view: no
  entities:
  - sensor.rotary_value

and customize.yaml for convenience:

sensor.rotary_value:
  friendly_name: Rotary Encoder Value
  icon: mdi:debug-step-over

Hope I haven’t left anything out.

Could be used for light dimming and other like things I suppose.

8 Likes

That’s a cool thing! I tried to do something similar with an eps8266 (esp-03, super small chip with many GPIO) to integrate directly in my wall sockets. I used mqtt to publish the rotary changes to home assistant and then to process those changes with some automation.

That was a complete no-go. If I turned the knob too many times or too fast the mqtt messages were not processed anymore. My goal was to have a generalized mqtt device that responds always in the same way and to do the interpretation in home assistant. This way I only have to change the mqtt topic per device in its code and upload it (arduino IDE with OTA updating).

I’ll try your approach by directly sending api requests to hass instead of processing them with an automation.

2 Likes

Just an update for anyone using Node RED:

The below will replace the automation.yaml entry listed above, also it requires one line changed in the code to read:

  remote.set_state(api, 'input_number.rotary_encoder_value', new_state=ro_count / 100)

[{"id":"fe6f5094.196098","type":"server-state-changed","z":"bebbe41b.7676e8","name":"input_number.rotary_encoder_value","server":"5c978c05.6f28cc","entityidfilter":"input_number.rotary_encoder_value","haltifstate":"unknown","x":340,"y":200,"wires":[["60b44e4.acf68b"]]},{"id":"32721c7d.7a8f94","type":"function","z":"bebbe41b.7676e8","name":"Parse New Volume Level","func":"newmsg = Object();\n\nold_vol = parseFloat(msg.payload);\nnew_vol = old_vol;\n\nnewmsg.payload = { data: { 'entity_id': 'media_player.hifi_pi','volume_level': new_vol } }\n\nreturn newmsg;","outputs":1,"noerr":0,"x":870,"y":280,"wires":[["7bc11ecf.de3c28"]]},{"id":"7bc11ecf.de3c28","type":"api-call-service","z":"bebbe41b.7676e8","name":"Adjust Volume","server":"d644e1ec.bf7188","service_domain":"media_player","service":"volume_set","data":"{\"entity_id\":\"media_player.hifi_pi\"}","x":1100,"y":320,"wires":[]},{"id":"60b44e4.acf68b","type":"api-current-state","z":"bebbe41b.7676e8","name":"Retrieve New Value","server":"d644e1ec.bf7188","halt_if":"","entity_id":"input_number.rotary_encoder_value","x":620,"y":240,"wires":[["32721c7d.7a8f94"]]},{"id":"efa4f374.ff13b","type":"comment","z":"bebbe41b.7676e8","name":"Adjust Volume with Rotary Encoder","info":"","x":340,"y":142,"wires":[]},{"id":"5c978c05.6f28cc","type":"server","z":"","name":"Home Assistant","url":"http://192.168.178.113:8123","pass":""},{"id":"d644e1ec.bf7188","type":"server","z":"","name":"Home Assistant","url":"http://localhost:8123","pass":"YOURPASS"}]

Many thanks to @oakbrad whose guides are the absolute business for getting to know Node Red with Hass :slight_smile:

1 Like

Pleaseeee share if you get this working :))
I have a few ESP8266 and rotary encoders laying around and would love a few knobs around the house to dim lights and adjust volume!!!

I know this thread is old, but I just implemented this with an esp8266 and esphome. The rotary encoder is really easy to setup, and there is nothing you need in configuration.yaml.

The automation in the first post worked out of the box, with the entity names changed to my setup.

Thanks.

2 Likes

I’d like to see your esphome yaml for the device – I’d like to have it call the home-assistant api instead of relying on node-red to perform the action.

Do you mind sharing?

esphome:
  name: wemos
  platform: ESP8266
  board: d1_mini

wifi:
  ssid: 'xxxx'
  password: 'xxxx'
  manual_ip:
    static_ip: 192.168.20.228
    gateway: 192.168.20.1
    subnet: 255.255.255.0
    dns1: 192.168.20.126

# Enable logging
logger:

# Enable Home Assistant API
api:
  password: 'xxxx'

ota:
  password: 'xxxx'
  
binary_sensor:
  - platform: status
    name: "Wemos Status"
    
  - platform: gpio
    pin:
      number: D7
      mode: INPUT_PULLUP
      inverted: true
    name: "Switch"
    
sensor:
  - platform: uptime
    name: Wemos Uptime
  
  - platform: wifi_signal
    name: "Wemos WiFi Signal Sensor"
    update_interval: 60s
    
  - platform: rotary_encoder
    name: "Rotary Encoder"
    pin_b: 
     number: D6
     mode: INPUT
    pin_a: 
     number: D5
     mode: INPUT
    max_value: 100
    min_value: 0
    resolution: 4

This uses api and creates an entity sensor.rotary_encoder. The automation is

- id: Adjust Volume on Rotary Encoder Move
  alias: "Rotary Volume Change"
  trigger:
    - platform: state
      entity_id: sensor.rotary_encoder
  action:
    - service: media_player.volume_set
      entity_id: media_player.kitchen
      data_template:
        volume_level: "{{ states.sensor.rotary_encoder.state | float / 100 }}"

There are issues, They get out of sync. I’ll fix it one day.

3 Likes