diff --git a/README.md b/README.md index 19ba3d4..a3a458d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,139 @@ -# ddc-mqtt +# ddc-mqtt - A simple software Display Input switch for DDC-supporting monitors -Setup: +[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](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 -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) -2. docker-compose up -d +- Do you have a displays with multiple inputs? +- Would you like to be able to switch inputs without the need of meddling with clunky OSD menus and akwardly-positioned control 'nipples'? +- 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! + +![Display KVM](https://raw.githubusercontent.com/moimart/ddc-mqtt/main/hass_display_kvm.jpg)' + +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!** \ No newline at end of file diff --git a/ddc-mqtt/devices.py b/ddc-mqtt/devices.py index becd4d1..220b336 100644 --- a/ddc-mqtt/devices.py +++ b/ddc-mqtt/devices.py @@ -1,6 +1,6 @@ device = { "identifiers": ["Kikkei Labs Display KVM"], - "name": "Display KVM", + "name": "Display KVM #?", "model": "Kikkei-display-kvm-0", "manufacturer": "Kikkei Labs", } @@ -8,7 +8,7 @@ device = { display_device = { "identifiers": ["Input"], - "name": "Display Input KVM", + "name": "Display Input KVM #?", "model": "Kikkei-display-kvm", "manufacturer": "Kikkei Labs", } diff --git a/ddc-mqtt/rename_to_config.yml b/ddc-mqtt/rename_to_config.yml index 8936c88..4f45c70 100644 --- a/ddc-mqtt/rename_to_config.yml +++ b/ddc-mqtt/rename_to_config.yml @@ -4,10 +4,10 @@ mqtt: host: port: 1883 display: - id: 1 - inputs: - hdmi1: 5 - hdmi2: 6 - hdmi3: 7 - dp: 15 - + - id: 1 + inputs: + HDMI1: 5 + HDMI2: 6 + HDMI3: 7 + DP: 15 +interval: 20 diff --git a/ddc-mqtt/start.py b/ddc-mqtt/start.py index 2552ced..eff7b85 100644 --- a/ddc-mqtt/start.py +++ b/ddc-mqtt/start.py @@ -23,6 +23,8 @@ class Service: config["mqtt"]["host"], config["mqtt"]["port"]) + poll_interval = 20 if "interval" not in config else config["interval"] + self.mqtt.delegate = self self.inputs = {} @@ -30,27 +32,24 @@ class Service: print(display_data) - self.inputs = { - display_data['id']: { - "switches": [] - } - } + self.inputs = {} - for input_name, input_code in display_data['inputs'].items(): - self.create_display_switch(display_data['id'],input_name,input_code) + for display in display_data: + 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() def update_inputs_states(self): - input_code = simpleddc.show_input() - - print(f"Input code is {input_code}") - for display_id in self.inputs.keys(): + input_code = simpleddc.show_input(int(display_id)) + + print(f"Input code is {input_code}") + for entry in self.inputs[display_id]["switches"]: if entry["code"] == input_code: - print("Found {}".format(entry["topic"])) self.mqtt.client.publish(entry["topic"], "true") entry["state"] = True else: @@ -101,7 +100,7 @@ class Service: config["state_topic"] = config["state_topic"].replace("?", input_name) 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) config["device"] = device diff --git a/hass_display_kvm.jpg b/hass_display_kvm.jpg new file mode 100644 index 0000000..71fc111 Binary files /dev/null and b/hass_display_kvm.jpg differ diff --git a/simpleddc-extension/simple-ddc.h b/simpleddc-extension/simple-ddc.h index dd6b97a..d7103f8 100644 --- a/simpleddc-extension/simple-ddc.h +++ b/simpleddc-extension/simple-ddc.h @@ -7,6 +7,7 @@ #include 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); uint8_t show_any_value( DDCA_Display_Handle dh, diff --git a/simpleddc-extension/simpleddc-python.c b/simpleddc-extension/simpleddc-python.c index 57b32ec..d580f01 100644 --- a/simpleddc-extension/simpleddc-python.c +++ b/simpleddc-extension/simpleddc-python.c @@ -19,9 +19,16 @@ static PyObject* switch_to_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); ddca_close_display(handle); + + free(display); return PyLong_FromLong(result); } diff --git a/simpleddc-extension/simpleddc.c b/simpleddc-extension/simpleddc.c index 2a89722..b4a2743 100644 --- a/simpleddc-extension/simpleddc.c +++ b/simpleddc-extension/simpleddc.c @@ -8,7 +8,8 @@ ddca_rc_desc(status_code)); \ } 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"); DDCA_Display_Handle dh = NULL; @@ -20,13 +21,39 @@ DDCA_Display_Handle * open_first_display_by_dlist() { if (dlist->ct == 0) { printf(" No DDC capable displays found\n"); - } - else { + } else { DDCA_Display_Info * dinf = &dlist->info[0]; DDCA_Display_Ref * dref = dinf->dref; printf("Opening display %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); if (rc != 0) { DDC_ERRMSG("ddca_open_display2", rc);