3d printed volume control device/dimmer input

Hello All

In our home we play music over chromecast audio devices, and usually use a google home to control such audio. But sometimes it can be hard for google to hear the operator because of interference from loud music, and then one needs to go looking for a mobile phone to turn down the music.

In order to combat this (admittedly first world) problem I have come up with a wireless volume dial that uses a Xiaomi tilt sensor.
In this example the tilt sensor integrated into a dial are used to make adjustments to the volume of a chromecast device, but in principle this could be used to adjust any item that takes a number input (dimmers, thermostats etc)

On the hardware side I have selected the Xiaomi tilt sensor, available from the usual suspects at a reasonable price. Using this sensor a few pieces of hardware have to be stuck together, and a little code written. No soldering, no wires, Xiaomi has done all the hard work :slight_smile:

From the top in the exploded view you see:

  • an outer housing, the dial, 3d printed
  • 3 small neodymium magnets
  • the Xiaomi tilt sensor
  • a bearing 6808-2RS
  • the base plate, 3d printed


A CAD model can be found here. STL files are available pre generated, and you are free to make copies and modify as you see fit.

The dial has a pocket for the tilt sensor, and 3 pockets for the small magnets. The magnets fix the dial to the outer race of the bearing.
The sensor can be kept in place with doublesided tape. The magnets with CA glue.
A couple of features have been included that can be shaved off to make a nice fit with the bearing, to account for possible shrinkage.

The bearing is fitted to the bottom plate and CA glued in place. A hole and a countersink are provided for mounting on a wall.

In this example the input from the dial is used to adjust the volume of a chromecast device. It could be light dimmer, a color selector, or any other number type item.

import java.lang.Math

rule "volume rule"
// this rule changes the volume of a chromecast device based on the input of a tilt sensor
    Item XiaomiAqaraSmartMotionSensor_XOrientationOfTheDevice changed
    logInfo("","volume rule running")
    //the item Volume angle is used to store the old state, much faster than looking up old states in persistance
    var prev_angle = Volume_angle.state as Number
    var x = XiaomiAqaraSmartMotionSensor_XOrientationOfTheDevice.state as Number
    var y = XiaomiAqaraSmartMotionSensor_YOrientationOfTheDevice.state as Number

    //use atan to calculate the angle of the sensor this does assume that the sensor is placed on a 
    //wall (rougly parallel to gravity). The x and y axis are along the long sides of the sensor, the 
    //z axis is along the short length
    var angle = Math.toDegrees(Math.atan2(x.doubleValue,y.doubleValue))

    //calculate the change in orientation
    var angle_change = angle-prev_angle

    //because we are using atan2 when passing 0 the atan2 function returns 360 degrees we need 
    //to subtract this or add this back to keep eardrums intact
    if( angle_change > 260 ) {
        angle_change = angle_change -360
        logInfo("","first correction")
    if( angle_change < -260 ) {
        angle_change = angle_change +360
        logInfo("","second correction")

    //update the sensor "old state"

    //these values are used to adjust the behaviour of the transformation of angles in degrees to a 
    //percent increase or decrease. I found by experimentation that both a proportional and an 
    //exponential correction works best, adjust the numbers to suit your application.
    val degrees_per_percent = 10
    val exponential_factor = 1.5

    //if you turn the dial up and the volume goes down, witch the sign of the number below.
    val rotation_direction_correction = -1

    var proportional_correction = angle_change/degrees_per_percent
    var sign = Math.signum(proportional_correction).intValue

    var percent_change = ((Math.abs(proportional_correction)**exponential_factor) * sign * rotation_direction_correction).intValue

    val device_volume = device_Volume.state as Number

    var new_volume_device = (device_volume + percent_change)

    //these if statements make sure that a non positive number of volume % are not sent
    // to the binding, as this is not permitted, and you basically have to hit the number right
    // on the head to get to 0.
    if( new_volume_device < 0 ) {
        new_volume_device = 0    

    //these if statements make sure that no number higher than 100 is sent to the % state of the "dimmer"
    // which would otherwise result in an error, furthermore this stops the user from overshooting and not getting
    // the desired volume increase (up to 100%)
    if( new_volume_device > 100 ) {
        new_volume_device = 100