Updated README and improved multimonitor support
This commit is contained in:
parent
bc4bc9afdb
commit
caa16c27d5
8 changed files with 199 additions and 33 deletions
142
README.md
142
README.md
|
|
@ -1,7 +1,139 @@
|
||||||
# ddc-mqtt
|
# ddc-mqtt - A simple software Display Input switch for DDC-supporting monitors
|
||||||
|
|
||||||
Setup:
|
[](https://www.buymeacoffee.com/moimartb)
|
||||||
|
|
||||||
0. Connect a device where to run this to a DDC enabled port of your monitor— I use a raspberry pi with a GN95C Samsung display
|
- Do you have a displays with multiple inputs?
|
||||||
1. create a config.yml (copy it from rename_to_config.yml) for your mqtt credentials and monitor setup (Use ddcutil to get inputs and codes)
|
- Would you like to be able to switch inputs without the need of meddling with clunky OSD menus and akwardly-positioned control 'nipples'?
|
||||||
2. docker-compose up -d
|
- Does this happen to you even with fancy, expensive displays like the Neo G9 line from Samsung?
|
||||||
|
- Can you imagine KVM scenarios based on home assistant scenarios?
|
||||||
|
|
||||||
|
Then this software is for you!!!
|
||||||
|
|
||||||
|
You only need a Linux machine connected to the display, a bit of knowledge on your monitor's support of DDC capabilities and this will
|
||||||
|
create a device for your
|
||||||
|
|
||||||
|
## Display's DDC capabilities
|
||||||
|
|
||||||
|
First of all, you'd need some info about your display about DDC capabilities wrt display inputs. You'd need the *ddcutil* tool to figure
|
||||||
|
things out. For example: this is what my G95NC Samsung Neo G9 Reports:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ddcutil capabilities
|
||||||
|
Model: FALCON
|
||||||
|
MCCS version: 2.0
|
||||||
|
Commands:
|
||||||
|
Op Code: 01 (VCP Request)
|
||||||
|
Op Code: 02 (VCP Response)
|
||||||
|
Op Code: 03 (VCP Set)
|
||||||
|
Op Code: 07 (Timing Request)
|
||||||
|
Op Code: 0C (Save Settings)
|
||||||
|
Op Code: E3 (Capabilities Reply)
|
||||||
|
Op Code: F3 (Capabilities Request)
|
||||||
|
VCP Features:
|
||||||
|
Feature: 02 (New control value)
|
||||||
|
Feature: 04 (Restore factory defaults)
|
||||||
|
Feature: 05 (Restore factory brightness/contrast defaults)
|
||||||
|
Feature: 08 (Restore color defaults)
|
||||||
|
Feature: 10 (Brightness)
|
||||||
|
Feature: 12 (Contrast)
|
||||||
|
Feature: 14 (Select color preset)
|
||||||
|
Values:
|
||||||
|
05: 6500 K
|
||||||
|
08: 9300 K
|
||||||
|
0b: User 1
|
||||||
|
0c: User 2
|
||||||
|
Feature: 16 (Video gain: Red)
|
||||||
|
Feature: 18 (Video gain: Green)
|
||||||
|
Feature: 1A (Video gain: Blue)
|
||||||
|
Feature: 52 (Active control)
|
||||||
|
Feature: 60 (Input Source)
|
||||||
|
Values:
|
||||||
|
01: VGA-1
|
||||||
|
03: DVI-1
|
||||||
|
04: DVI-2
|
||||||
|
11: HDMI-1
|
||||||
|
12: HDMI-2
|
||||||
|
0f: DisplayPort-1
|
||||||
|
10: DisplayPort-2
|
||||||
|
Feature: 62 (Audio speaker volume)
|
||||||
|
Feature: 8D (Audio Mute)
|
||||||
|
Feature: FF (Manufacturer specific feature)
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll focus on the **Feature: 60 (Input Source)** ... I have good news and bad news:
|
||||||
|
|
||||||
|
1. Good news: The display supports changing inputs with DDC!
|
||||||
|
2. Bad news: **The inputs and the values are, for the most part, are bullsh\*t!!**
|
||||||
|
|
||||||
|
Because of this, we will need to figure out the actual input values for each actual input of the monitor.
|
||||||
|
|
||||||
|
You can use **ddcutil** again to figure them out like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
# change the last number to figure the actual values that change the input. Have in mind these are hexadecimal values you'll need to convert to integers
|
||||||
|
$ sudo ddcutil setvcp 0x60 0x01
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Once you have the values for the inputs you want to control, you can fill out the config.yml inside the folder ddc-mqtt that you'd need for the software to run. For my display this would be:
|
||||||
|
|
||||||
|
```
|
||||||
|
mqtt:
|
||||||
|
username: #your mqtt server's username
|
||||||
|
password: #your mqtt server's password
|
||||||
|
host: 1.2.3.4 #your mqtt server's host
|
||||||
|
port: 1883
|
||||||
|
display:
|
||||||
|
- id: 1 #display to control. if only a single monitor, this must be 1
|
||||||
|
inputs:
|
||||||
|
HDMI1: 5 # key: value — you can change the name of the input. The codes here are as integers, not hexadecimal
|
||||||
|
HDMI2: 6
|
||||||
|
HDMI3: 7
|
||||||
|
DP: 15
|
||||||
|
interval: 20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
You might want to use the docker container that comes with the code. If you use a Raspberry Pi you just do:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
And if your config is well formed, that should be it!
|
||||||
|
|
||||||
|
If you don't use a Raspberry Pi, you might need to pay attention to the *docker-compose.yml* in the root folder as we are
|
||||||
|
forwarding the i2c devices for the ddcutil library to access.
|
||||||
|
|
||||||
|
```
|
||||||
|
devices:
|
||||||
|
- "/dev/i2c-20:/dev/i2c-20"
|
||||||
|
- "/dev/i2c-21:/dev/i2c-21" #these might be different in your machine
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
If your MQTT server is shared or is part of a *Home Assistant* setup, the usage is simple. You'll get a device with switches to change inputs and that's all!
|
||||||
|
|
||||||
|
'
|
||||||
|
|
||||||
|
If you want to use it MQTT RAW!!! you'd have access to the entities in the following paths:
|
||||||
|
|
||||||
|
```
|
||||||
|
Subscribe to kikkei/display-kvm/{number_of_display}/{input_name_in_the_config}/state -> with values 'true' or 'false' to check the state
|
||||||
|
Publish 'ON' to kikkei/display-kvm/{number_of_display}/{input_name_in_the_config}/command for activation -> no need to send 'OFF'; switching is taken care of
|
||||||
|
|
||||||
|
Example: kikkei/display-kvm/1/HDMI1/state
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observations
|
||||||
|
|
||||||
|
This software is polling at an interval to re-check of the state of the active input— just in case you switch it manually...
|
||||||
|
|
||||||
|
This interval can be configured in the config.yml with the key interval. It is optional and by default is 20 seconds...
|
||||||
|
|
||||||
|
I coded this to support multiple monitors but I have not tested it at all... contributions are very welcome! :)
|
||||||
|
|
||||||
|
**Hope it works for you as well as it works for me!**
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
device = {
|
device = {
|
||||||
"identifiers": ["Kikkei Labs Display KVM"],
|
"identifiers": ["Kikkei Labs Display KVM"],
|
||||||
"name": "Display KVM",
|
"name": "Display KVM #?",
|
||||||
"model": "Kikkei-display-kvm-0",
|
"model": "Kikkei-display-kvm-0",
|
||||||
"manufacturer": "Kikkei Labs",
|
"manufacturer": "Kikkei Labs",
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ device = {
|
||||||
|
|
||||||
display_device = {
|
display_device = {
|
||||||
"identifiers": ["Input"],
|
"identifiers": ["Input"],
|
||||||
"name": "Display Input KVM",
|
"name": "Display Input KVM #?",
|
||||||
"model": "Kikkei-display-kvm",
|
"model": "Kikkei-display-kvm",
|
||||||
"manufacturer": "Kikkei Labs",
|
"manufacturer": "Kikkei Labs",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ mqtt:
|
||||||
host:
|
host:
|
||||||
port: 1883
|
port: 1883
|
||||||
display:
|
display:
|
||||||
id: 1
|
- id: 1
|
||||||
inputs:
|
inputs:
|
||||||
hdmi1: 5
|
HDMI1: 5
|
||||||
hdmi2: 6
|
HDMI2: 6
|
||||||
hdmi3: 7
|
HDMI3: 7
|
||||||
dp: 15
|
DP: 15
|
||||||
|
interval: 20
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ class Service:
|
||||||
config["mqtt"]["host"],
|
config["mqtt"]["host"],
|
||||||
config["mqtt"]["port"])
|
config["mqtt"]["port"])
|
||||||
|
|
||||||
|
poll_interval = 20 if "interval" not in config else config["interval"]
|
||||||
|
|
||||||
self.mqtt.delegate = self
|
self.mqtt.delegate = self
|
||||||
|
|
||||||
self.inputs = {}
|
self.inputs = {}
|
||||||
|
|
@ -30,27 +32,24 @@ class Service:
|
||||||
|
|
||||||
print(display_data)
|
print(display_data)
|
||||||
|
|
||||||
self.inputs = {
|
self.inputs = {}
|
||||||
display_data['id']: {
|
|
||||||
"switches": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for input_name, input_code in display_data['inputs'].items():
|
for display in display_data:
|
||||||
self.create_display_switch(display_data['id'],input_name,input_code)
|
self.inputs[display['id']] = { "switches": [] }
|
||||||
|
for input_name, input_code in display['inputs'].items():
|
||||||
|
self.create_display_switch(display['id'],input_name,input_code)
|
||||||
|
|
||||||
self.timer = Timer(30, self)
|
self.timer = Timer(poll_interval, self)
|
||||||
self.update_inputs_states()
|
self.update_inputs_states()
|
||||||
|
|
||||||
def update_inputs_states(self):
|
def update_inputs_states(self):
|
||||||
input_code = simpleddc.show_input()
|
for display_id in self.inputs.keys():
|
||||||
|
input_code = simpleddc.show_input(int(display_id))
|
||||||
|
|
||||||
print(f"Input code is {input_code}")
|
print(f"Input code is {input_code}")
|
||||||
|
|
||||||
for display_id in self.inputs.keys():
|
|
||||||
for entry in self.inputs[display_id]["switches"]:
|
for entry in self.inputs[display_id]["switches"]:
|
||||||
if entry["code"] == input_code:
|
if entry["code"] == input_code:
|
||||||
print("Found {}".format(entry["topic"]))
|
|
||||||
self.mqtt.client.publish(entry["topic"], "true")
|
self.mqtt.client.publish(entry["topic"], "true")
|
||||||
entry["state"] = True
|
entry["state"] = True
|
||||||
else:
|
else:
|
||||||
|
|
@ -101,7 +100,7 @@ class Service:
|
||||||
config["state_topic"] = config["state_topic"].replace("?", input_name)
|
config["state_topic"] = config["state_topic"].replace("?", input_name)
|
||||||
|
|
||||||
device = display_device.copy()
|
device = display_device.copy()
|
||||||
device["name"] = device["name"].replace("#", input_name)
|
device["name"] = device["name"].replace("?", input_name)
|
||||||
device["model"] = "{}-{}".format(device["model"],display_id)
|
device["model"] = "{}-{}".format(device["model"],display_id)
|
||||||
config["device"] = device
|
config["device"] = device
|
||||||
|
|
||||||
|
|
|
||||||
BIN
hass_display_kvm.jpg
Normal file
BIN
hass_display_kvm.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
|
|
@ -7,6 +7,7 @@
|
||||||
#include <ddcutil_status_codes.h>
|
#include <ddcutil_status_codes.h>
|
||||||
|
|
||||||
DDCA_Display_Handle * open_first_display_by_dlist();
|
DDCA_Display_Handle * open_first_display_by_dlist();
|
||||||
|
DDCA_Display_Handle * open_display_by_dlist(unsigned int display);
|
||||||
DDCA_Status switch_input(DDCA_Display_Handle* handle, uint8_t input);
|
DDCA_Status switch_input(DDCA_Display_Handle* handle, uint8_t input);
|
||||||
uint8_t show_any_value(
|
uint8_t show_any_value(
|
||||||
DDCA_Display_Handle dh,
|
DDCA_Display_Handle dh,
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,16 @@ static PyObject* switch_to_input(PyObject* self, PyObject* args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject* show_input(PyObject* self, PyObject* args) {
|
static PyObject* show_input(PyObject* self, PyObject* args) {
|
||||||
DDCA_Display_Handle* handle = open_first_display_by_dlist();
|
uint8_t* display = malloc(sizeof(uint8_t));
|
||||||
|
|
||||||
|
if (!PyArg_ParseTuple(args, "i", display))
|
||||||
|
*display = 0;
|
||||||
|
|
||||||
|
DDCA_Display_Handle* handle = open_display_by_dlist(*display);
|
||||||
int result = show_any_value(handle,DDCA_NON_TABLE_VCP_VALUE, 0x60);
|
int result = show_any_value(handle,DDCA_NON_TABLE_VCP_VALUE, 0x60);
|
||||||
ddca_close_display(handle);
|
ddca_close_display(handle);
|
||||||
|
|
||||||
|
free(display);
|
||||||
return PyLong_FromLong(result);
|
return PyLong_FromLong(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
ddca_rc_desc(status_code)); \
|
ddca_rc_desc(status_code)); \
|
||||||
} while(0)
|
} while(0)
|
||||||
|
|
||||||
DDCA_Display_Handle * open_first_display_by_dlist() {
|
DDCA_Display_Handle * open_first_display_by_dlist()
|
||||||
|
{
|
||||||
printf("Check for monitors using ddca_get_displays()...\n");
|
printf("Check for monitors using ddca_get_displays()...\n");
|
||||||
DDCA_Display_Handle dh = NULL;
|
DDCA_Display_Handle dh = NULL;
|
||||||
|
|
||||||
|
|
@ -20,13 +21,39 @@ DDCA_Display_Handle * open_first_display_by_dlist() {
|
||||||
|
|
||||||
if (dlist->ct == 0) {
|
if (dlist->ct == 0) {
|
||||||
printf(" No DDC capable displays found\n");
|
printf(" No DDC capable displays found\n");
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
DDCA_Display_Info * dinf = &dlist->info[0];
|
DDCA_Display_Info * dinf = &dlist->info[0];
|
||||||
DDCA_Display_Ref * dref = dinf->dref;
|
DDCA_Display_Ref * dref = dinf->dref;
|
||||||
printf("Opening display %s\n", dinf->model_name);
|
printf("Opening display %s\n", dinf->model_name);
|
||||||
printf("Model: %s\n", dinf->model_name);
|
printf("Model: %s\n", dinf->model_name);
|
||||||
//printf("Model: %s\n", dinf->mmid.model_name);
|
|
||||||
|
DDCA_Status rc = ddca_open_display2(dref, false, &dh);
|
||||||
|
if (rc != 0) {
|
||||||
|
DDC_ERRMSG("ddca_open_display2", rc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ddca_free_display_info_list(dlist);
|
||||||
|
return dh;
|
||||||
|
}
|
||||||
|
|
||||||
|
DDCA_Display_Handle * open_display_by_dlist(unsigned int display)
|
||||||
|
{
|
||||||
|
DDCA_Display_Handle dh = NULL;
|
||||||
|
|
||||||
|
// Inquire about detected monitors.
|
||||||
|
DDCA_Display_Info_List* dlist = NULL;
|
||||||
|
ddca_get_display_info_list2(
|
||||||
|
false, // don't include invalid displays
|
||||||
|
&dlist);
|
||||||
|
|
||||||
|
if (dlist->ct == 0 || dlist->ct < display) {
|
||||||
|
printf(" No DDC capable displays found\n");
|
||||||
|
} else {
|
||||||
|
DDCA_Display_Info * dinf = &dlist->info[display - 1];
|
||||||
|
DDCA_Display_Ref * dref = dinf->dref;
|
||||||
|
printf("Opening display %s\n", dinf->model_name);
|
||||||
|
printf("Model: %s\n", dinf->model_name);
|
||||||
|
|
||||||
DDCA_Status rc = ddca_open_display2(dref, false, &dh);
|
DDCA_Status rc = ddca_open_display2(dref, false, &dh);
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
DDC_ERRMSG("ddca_open_display2", rc);
|
DDC_ERRMSG("ddca_open_display2", rc);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue