How to: Universal Media player to control LMS squeezelite and old amplifier

If you build a full house audio system and you want to take advantage of some existing audio equipment you may need to be creative to make your old components smart. In my setup I have a 30+ year old stereo system. It works great, but it has no network or IR control abilities.

The minimal features required for a smart media player are the ability to turn it on and off and to control the volume. Turning the amp on and off can be handled using a smart plug. Changing the volume, that’s a little more of a challenge. I built a cat feeder that I integrated with HA that utilizes servos and an electric motor. Both seemed like reasonable options for turning the volume knob.

The amplifier volume knob moves about 240 degrees from mute to max volume. This means the controlling hardware doesn’t have to do a full rotation. The controlling device need to go in both directions. This is easier to do with a servo, as that’s how servos are designed to work. Based on this I figured I’d start this effort trying to use a servo. This is the servo I used. At this point it appears to be working fine. While it’s not pretty it’s functional as seen below.

The chain and wheels used to connect the server to the amp came from here.

The rasberry pi was in place running the LMS squeezelite media player providing audio to the amp. Connected to the raspberry pi gpio pins is this PCA9685 controller from adafruit. This controller will drive 16 servos and so is overkill. I suspect you could find alternative hardware.

The raspberry pi software required to run the servo is as follows:

apt-get install python3-smbus i2c-tools 
pip3 install adafruit-pca9685
pip3 install adafruit-circuitpython-servokit

Using raspi-config you need to enable I2C under interface options.

The following script named amp_vol_ctrl.py will create a server that listens for network commands to change the volume:

#!/usr/bin/python3
# Use PCA9685 PWM servo/LED controller library to control servos
# License: Public Domain
from __future__ import division
import time
import sys
import socket
import random
import datetime
import math

from board import SCL, SDA
import busio
from adafruit_pca9685 import PCA9685
from adafruit_motor import servo

# Uncomment to enable debug output.
#import logging
#logging.basicConfig(level=logging.DEBUG)

freq = 60  # number of pulses per second 
VERT_CHAN = 1 
vert_delay = .02

def turn_off_servos():
    #pca.reset()
    vertServo._pwm_out.duty_cycle = 0
    print("Turn off servo")

# -- Main ----
for arg in sys.argv[1:]:
    print( arg )

i2c = busio.I2C(SCL, SDA)

# Create a simple PCA9685 class instance.
pca = PCA9685(i2c)
pca.frequency = freq 

vertServo = servo.Servo(pca.channels[VERT_CHAN], min_pulse=500, max_pulse=2400)

# turn off servos
turn_off_servos()

HOST = ''                 # Symbolic name meaning all available interfaces
PORT = 33333              # Arbitrary non-privileged port
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen(1)

    while True:
        try:
            conn, addr = s.accept()
            with conn:
                print('Connected by', addr)
                while True:
                    data = conn.recv(1024).decode("ascii")
                    if not data: break
                    print('Data: ',data)
                    level = float(data)
                    if level < 0:
                        level = 100
                    elif level <= 1:
                        level = int( 100 - (level * 100) )
                    elif level > 100:
                        level = 100
                    else:
                        level = int(100 - level)

                    print('Level',level)
                    angle = int(level * 9 / 5)  
                    if angle < 0:
                       angle=0 
                    elif angle > 180:
                       angle=180
                    print('Angle',angle)

                    vertServo.angle = angle
                    #time.sleep(vert_delay)
                    #set_pulse_length( chr(data[0]) , 10 )

                conn.close()
                sys.stdout.flush()

        except Exception as e:
            logging.error(traceback.format_exc())

        finally:
            # turn off servos
            turn_off_servos()

Assuming you place this file in /usr/local/bin with permissions 755, you can start the script by placing the following in /etc/rc.local:

runuser -l pi -c '/usr/local/bin/amp_vol_ctrl.py  >/dev/null 2>&1 &'

Reboot the system and if all goes well you should have a server listening on TCP port 33333.
If you have the servo connected to pin set 1 of the PCA9685 board you can test moving the servo with the following command:

echo -n 0.00 | nc -w 0  127.0.0.1 33333

The value 0.00 should move all the way in one direction. A value of 1.00 will move all the way in the other direction. The range of value accepted are 0.00 - 1.00. So to move 20% you use “0.20” in the echo command.

Putting the HA modiciations in place to control the volume
Through out the information provided below you will have to adjust entity names to match what is on your system as I don’t expect you’ll have the same names I’m using. You will notice living_room is used a lot in the names, that’s because the amp is in my living room.

These instructions also have you edit .yaml files. Before you edit a .yaml file I would suggest you copy the file. This gives you protection just in case something really gets messed up.

I user a few things to give the appropriate control of the media player.

  • First I use the squeezebox integration that gives me media play control of each squeezelite media players in my whole house audio system.
  • I use the TP-Link Kasa Smart integration that gives me the on/off control of the amp plug. If you system is doing auto discovery of smart devices it should find the plug once it’s on your network.
  • I use a input_number to store the volume level
  • I use a python_script to save the adjusted volume levels in the state field of the input_number
  • I use a the universal media player to join the smart plug, servo volume control and squeezelite player attributes
  • I use a shell_command to communicate from HA to the volume control server
  • I use an HA script to call the shell_command and call the python_script to save the volume level

To use the python_script you must enable this in you configuration.yaml file by adding the line:

python_script

This causes HA to look for python_scripts in the config/python_script directory. The directory python_script doesn’t exist by default and you must create it. In the python_script directory you need the script that will update state information. It should be named set_state.py and the contents are as follow:

if 'entity_id' not in data:
  logger.warning("===== entity_id is required if you want to set something.")
else:
  data = data.copy()
  inputEntity = data.pop('entity_id')
  inputStateObject = hass.states.get(inputEntity)
  if inputStateObject:
    inputState = inputStateObject.state
    inputAttributesObject = inputStateObject.attributes.copy()
  else:
    inputState = 'unknown'
    inputAttributesObject = {}
  if 'state' in data:
    inputState = data.pop('state')
  logger.debug("===== new attrs: {}".format(data))
  inputAttributesObject.update(data)

  hass.states.set(inputEntity, inputState, inputAttributesObject)

This script should have permission 755. I don’t know where I found this version of the script so I can’t give credit. It seems there is a version that can be install via HACS. It’s probably worth trying to use it instead of the version I provided above.

To create the input_number that holds the volume level I edit input_number.yaml file and add these lines:

living_room_vol:
  name: living_room_vol
  min: 0
  max: 1
  step: .01

I added the HA script by editing scripts.yaml file and adding these lines:

living_room_set_vol:
  alias: living_room_set_vol
  sequence:
  - service: shell_command.living_room_amp_ctl
    data:
      level: "{{ level }}"
  - service: python_script.set_state
    data:
      entity_id: input_number.living_room_vol
      state: "{{ level }}"
  mode: single

You see in this script it calls the set_state python script we installed above. This script also calls a shell_command we need to set up. I put all of my shell scripts in the directory config/shell_cmds. The directory shell_cmds does not exist and so you need to create it. In the shell_cmds directory we want to create the file living_room_amp_ctl with the following contents:

#!/bin/bash
echo $1 | nc -w0 IP_ADDRESS_OF_SQUEEZELITE_PI 33333

You need to change IP_ADDRESS_OF_SQUEEZELITE_PI to the appropriate address of your pi. This file should also have permissions of 755.

I believe you need to give HA permission to access the shell_cmds directory so you need to add the following lines to your configuration.yaml file:

homeassistant:
  allowlist_external_dirs:
    - '/config/shell_cmds/'

If you already have allowlist_external_dirs you just need to add the /config/shell_cmds/ at the end of your existing list.

To make this shell script available to HA you need to edit configuration.yaml and add the following lines:

shell_command:
  living_room_amp_ctl: '/config/shell_cmds/living_room_amp_ctl {{ level }}'

The last thing is to tie things together with a universal media play that we also configure in the configuration.yaml file. You need the following lines:

media_player:
  - platform: universal
    name: livingroom_amp_cmb
    children:
      - media_player.livingroom_snap
      - switch.living_room_amp
    commands:
      turn_on:
        service: switch.turn_on
        target:
          entity_id: switch.living_room_amp
      turn_off:
        service: switch.turn_off
        target:
          entity_id: switch.living_room_amp
      volume_set:
        service: script.living_room_set_vol
        data:
          level: "{{ volume_level }}"
      volume_mute:
        service: media_player.volume_mute
        target:
          entity_id: media_player.livingroom_snap
        data:
          is_volume_muted: "{{ is_volume_muted }}"
      media_play_pause:
        service: media_player.media_play_pause
        target:
          entity_id: media_player.livingroom_snap

    attributes:
      state: switch.living_room_amp
      is_volume_muted: media_player.livingroom_snap|is_volume_muted
      volume_level: input_number.living_room_vol
      media_content_id: media_player.livingroom_snap|media_content_id
      media_title: media_player.livingroom_snap|media_title
      entity_picture: media_player.livingroom_snap|entity_picture

The above universal media play provides the following features when used with a mini media player card in the GUI.

  • The power button will turn on and off the TP-LINK plug that powers the amp
  • When the amp is on you get a slider bar for the volume control. Changing the slider will send commands to the RPI to move the servo and control the volume.
  • You get skip back, play/pause and skip forward buttons that control the squeezelite media player.
  • The mute button mutes the audio output of the squeezelite media player.

To validate these changes you can use the Developer Tools → YAML tab → CHECK CONFIGURATION option. Assuming no errors are found you’ll need to use the Developer Tools → YAML tab → RESTART link to activate the change. If all goes well you’ll have the media player livingroom_amp_cmb that you can add to the GUI or just access from the Settings-> Devices & Service → Entities tab to pull of the entity and try it out.

3 Likes

Now you need to build a robot to change the cassettes!

I’m afraid the cassette player only purpose is to raise the amp up 6 inches. It’s funny there is still a cassette in that player that I bet hasn’t been played in 25+ years. I do have a six cd disk player also in that stack. If I hadn’t digitized all my CDs 15 years ago, building a robot for it might be worth it. The main reason for keeping the stack is the amp still sound good and there’s a vinyl record player at the top. Playing vinyl once in a while still gives some strange enjoyment.

I keep buying vinyl even though I have no turntable. It helps keep them in mint condition.