Controlling Samsung Signage QB13R-T based on Unifi Protect

Result of this series as three minutes Youtube video: Data from home – Info Display
Details for the display: Controlling Samsung Signage QB13R-T based on Unifi Protect

Data from home – setup and preliminary results (Part 1)
Data from home – wireless water meter (Part 2)
Data from home – energy analyzer, requests against webUI (Part 3)
Data from home – district heating, computer vision (Part 4)
Data from home – alerting and water shutoff (Part 5)

I hope you enjoy!


Earlier this year I wrote 5 posts about gathering and visualizing the data from our home. I think I got some valuable results. Having done such effort I felt that I could put the results for a better use. That means having a info display at a wall of our home.

I had already seen people writing about using regular tablets as a wall display. I wasn’t so exited about the idea. So I searched for other alternatives. That is when I came across the Samsung 13″ Signage display with the touch functionality. The exact model is Samsung QB13R-T. It is designed for a professional use and 16/7 power on hours.

That one seems to have a quite good features. It’s reasonably sized, quite low on energy usage, has a touch screen, versatile media support and remote management features. In a way it’s like that tablet approach, since it has also a built-in browser with WLAN to present my Grafana dashboards. The first exiting task was to cut a hole for the display.

Convenient way to see inside the wardrobe without sliding the doors open? Actually a hole ready for my info display. Not perfect, but good enough!

I have owned this display for a week now. I still think it is really good fit for my use. Unfortunately I have also some shortcomings to mention. I would like it even more if it would have also Power over Ethernet (PoE) capability and the display should be more resistant to fingerprints. The built in MagicINFO player would be exiting but the premium license would cost almost the same as the display itself. With that one I could have preset multiple channels (Grafana, Photo slideshow…) and it also supports RTSP streaming so I could have my surveillance cameras as their own channels as well.

The first thing to overcome is the screen burning. I’m a bit conscious about leaving high contrast Grafana dashboars static for a longer periods. Since I’m also a photography hobbyist the natural solution is to show some nice photos every now and then. Actually right now I have a Grafana playlist that consist of one actual dashboard and one random photo. I’m changing between these two every 20 seconds.

Even the display is approved for 16/7 use I don’t like having it statically scheduled like that. It would be a waste of energy and just unnecessary. I’m trying to be more clever by controlling the power state not only by clock but also by motion close to the display. I’m using my Ubiquiti Unifi Protect and attached camera for that one. Since Unifi Protect doesn’t have a documented API, I will again have to use a simulated browser in means of a Python script to read the last detected motion information.

Then I will have to use that information to actually power on or power off the display. I did find multiple projects like samsungctl which are supposed to allow that functionality with Samsung TV’s. But I did’t manage to get those to work with my display.

This display can be categorized as LFD witch I suppose stands for a Large Format Display. This model being a 13″ version it seems a bit inaccurate. It happens to be that Samsung LFD displays do support MDC (Multi Display Control) protocol. Again, in my case a bit misleading, since I have only one, but lets not get confused by that fact. The “Smart Signage User Manual (Common Use)” has a good documentation on the protocol so I made a Python script to test it. And then I combined it with reading the motion detection information from the Unifi protect. My display is connected to WLAN.

## Simplified script to read lastmotion timestamp from Unifi Protect and control Samsung Signage LFD based on that one. 

## TODO: Proper JSON handling, display state awareness

import requests, re, time, json, socket

LOGIN_URL0 = "https://UNIFIPROTECTIP:PORT/login"
LOGIN_URL = "https://UNIFIPROTECTIP:PORT/api/auth"
API1 = "https://UNIFIPROTECTIP:PORT/api/bootstrap"

TCP_IP = 'LFDIP'
TCP_PORT = 1515
BUFFER_SIZE = 1024

## Memory for last command to avoid sending the same command indefinitely

last_command = 0

## INFINITE LOOP

n = 1
while n > 0:
    client = requests.session()
    
    ## Login to acquire access token

    login_data = json.dumps({"username":"USERNAME", "password":"PASSWORD"}).encode('utf-8')
    headers = {'Content-type': 'application/json; charset=utf-8', 'Accept': 'application/json'}

    r1 = client.post(LOGIN_URL, data=login_data, headers=headers, verify=False)

    print(r1.status_code)
    auth_token = r1.headers['authorization']
    
    ## Build access header to be used by GET

    auth_header = {'Authorization': 'Bearer ' + auth_token}

    ## Request camera information

    r1 = client.get(API1, headers=auth_header, verify=False)

    ## Quick and dirty regex to acquire lastmotion timestamp. Should be handled properly as JSON.

    camera_lastmotion = int("".join(re.findall('(?<="mac":"CAMERAMAC".{466})\d{10}', r1.text)))

    print(camera_lastmotion)

    epoch_time = int(time.time())

    print(epoch_time)

    ## Poweron when motion is detected

    if (epoch_time < (camera_lastmotion + 600) and last_command == 0):
        print("POWERON")

        
## 0xAA - Header
        
## 0x11 - Command type - Power control
        
## 0x00 - ID of my display
        
## 0x01 - Data length - 1 meaning send
        
## 0x01 - Data - 1 meaning power on
        
## 0x13 - Checksum - "The checksum is calculated by adding up all values except the header. If a checksum adds up to be more than 2
digits, the first digit is removed." - (11+00+01+01=13)

        data = [0xAA, 0x11, 0x00, 0x01, 0x01, 0x13]
        print(bytes(data))

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((TCP_IP, TCP_PORT))
        s.send(bytes(data))
        ack = s.recv(BUFFER_SIZE)
        s.close()

        last_command = 1


  ## Poweroff when there has been no motion for 15 minutes

  if (epoch_time > (camera_lastmotion + 3600) and last_command == 1):
        print("POWEROFF")
        data = [0xAA, 0x11, 0x00, 0x01, 0x00, 0x12]
        print(bytes(data))

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((TCP_IP, TCP_PORT))
        s.send(bytes(data))
        ack = s.recv(BUFFER_SIZE)
        s.close()

        last_command = 0