diff --git a/README.md b/README.md index 7743651..45aece1 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ - [Management Panel](#management-panel) - [Setting up](#setting-up) - [Home Assistant](#home-assistant) - - [CONFIGURATION VARIABLES](#configuration-variables) + - [CONFIGURATION VARIABLES (For unified_remote)](#configuration-variables-for-unified_remote) + - [CONFIGURATION VARIABLES (For each host)](#configuration-variables-for-each-host) - [Getting Started](#getting-started) - [How it works](#how-it-works) - [Remote description](#remote-description) @@ -23,8 +24,6 @@ - [Remotes section](#remotes-section) - [Types section](#types-section) - [How to use](#how-to-use) - - [Service](#service) - - [Switch Platform](#switch-platform) - [Contribute](#contribute) - [Submit Feedback](#submit-feedback) @@ -83,10 +82,31 @@ On your `configuration.yaml` file, add the following lines: ```yaml unified_remote: - host: UNIFIED_REMOTE_SERVER_IP + hosts: + - host: UNIFIED_REMOTE_SERVER_IP ``` -#### CONFIGURATION VARIABLES +#### CONFIGURATION VARIABLES (For unified_remote) + +**hosts** + +*(list)(Required)* + +List of hosts + +============================================ + +**retry_delay** + +*(number)(Optional)* + +Time to retry connection and/or keep it alive (seconds). You **CANNOT** set a delay greater than 120 seconds, this is the max value that will keep the connection alive. + +*Default value:* + +retry_delay: 120 + +#### CONFIGURATION VARIABLES (For each host) **host** *(string)(Required)* @@ -107,15 +127,15 @@ port: 9510 ============================================ -**retry_delay** +**name** -*(number)(Optional)* +*(string)(Optional)* -Time to retry connection and/or keep it alive (seconds). You **CANNOT** set a delay greater than 120 seconds, this is the max value that will keep the connection alive. +A name to identify the host. If unset, the name will be the host ip. *Default value:* -retry_delay: 120 +name: \ ## Getting Started @@ -135,7 +155,7 @@ Example: - If you want to turn off your computer, you need to use this remote ``` -remote_id: Unified.Power +id: Unified.Power action: turn_off ``` @@ -393,8 +413,6 @@ With the previous cases, we just declared 2 media_video remotes, but think if we Now we got a configured integration and declarated remotes, we have to be able to execute these action using remotes, of course, inside of Home Assistant. -### Service - This integration actualy register a service, called by `unified_remote.call` That service allows you to call your remotes. @@ -417,7 +435,15 @@ remote_id: remote_id action: remote_action ``` -For example: +- To specify which computer will receive the command, just add a `target` entry: + +```yaml +target: computer_name +remote_id: remote_id +action: remote_action +``` + +Example: This call will open Amazon Prime Video on my default browser. @@ -433,6 +459,22 @@ remote_id: Unified.AmazonPrimeVideo action: launch ``` +The same but specifying a computer by name: + +```yaml +target: PcMasterRace +remote_id: Unified.AmazonPrimeVideo +action: launch +``` + +If you didn't assign a computer name, the name will be same as computer ip, so: + +```yaml +target: 192.168.1.2 +remote_id: Unified.AmazonPrimeVideo +action: launch +``` + - For adding buttons on your home assistant lovelace, use `Manual Card` element with `call-service` action, like: ```yaml @@ -456,30 +498,10 @@ That example will restart my computer if I tap the button, after I accept the co ![demo-card](images/demo-card.png) -### Switch Platform - -It also comes with a switch platform, so you can define which action will be performed when swith goes on, off, or being (toggle options are little buggy) - -The following example is a switch that controls my monitor screen - -```yaml -switch: - - platform: unified_remote - name: "computer_screen" - turn_on: - remote: monitor - action: turn_on - turn_off: - remote: monitor - action: turn_off -``` - -![demo-switch](images/demo-switch.png) - ## Contribute Contributions are always welcome! -If you need some light, read some of following guides: +If you need some light, read some of the following guides: - [The beginner's guide to contributing to a GitHub project](https://akrabat.com/the-beginners-guide-to-contributing-to-a-github-project/) - [First Contributions](https://github.com/firstcontributions/first-contributions) - [How to contribute to open source](https://github.com/freeCodeCamp/how-to-contribute-to-open-source) @@ -487,6 +509,6 @@ If you need some light, read some of following guides: ## Submit Feedback -Be free to [open an issue](https://github.com/DaviPtrs/hass-unified-remote/issues/new/choose) telling your experience, suggesting new features or asking questions (there's no stupid questions, but make sure that yours cannot be answered by just reading docs) +Be free to [open an issue](https://github.com/DaviPtrs/hass-unified-remote/issues/new/choose) telling your experience, suggesting new features or asking questions (there's no stupid questions, but make sure that yours cannot be answered by just reading the docs) You can also find me on LinkedIn [/in/davipetris](https://www.linkedin.com/in/davipetris/) diff --git a/custom_components/unified_remote/__init__.py b/custom_components/unified_remote/__init__.py index 477ad19..45926e4 100644 --- a/custom_components/unified_remote/__init__.py +++ b/custom_components/unified_remote/__init__.py @@ -2,25 +2,33 @@ import logging as log from datetime import timedelta -from requests import ConnectionError - import homeassistant.helpers.config_validation as cv import voluptuous as vol -from custom_components.unified_remote.cli.connection import Connection -from custom_components.unified_remote.cli.remotes import Remotes -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_HOSTS, CONF_NAME, CONF_PORT from homeassistant.helpers.event import track_time_interval +from requests import ConnectionError -DOMAIN = "unified_remote" +from custom_components.unified_remote.cli.computer import Computer +from custom_components.unified_remote.cli.remotes import Remotes +DOMAIN = "unified_remote" CONF_RETRY = "retry_delay" CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Required(CONF_HOST, default="localhost"): cv.string, - vol.Optional(CONF_PORT, default="9510"): cv.string, + vol.Required(CONF_HOSTS): vol.Schema( + vol.All( + [ + { + vol.Optional(CONF_NAME, default=""): cv.string, + vol.Required(CONF_HOST, default="localhost"): cv.string, + vol.Optional(CONF_PORT, default="9510"): cv.port, + } + ] + ) + ), vol.Optional(CONF_RETRY, default=120): int, } ) @@ -45,13 +53,29 @@ except Exception as error: _LOGGER.error(str(error)) -CONNECTION = Connection() +COMPUTERS = [] + + +def init_computers(hosts): + for computer in hosts: + name = computer.get(CONF_NAME) + host = computer.get(CONF_HOST) + port = computer.get(CONF_PORT) + + if name == "": + name = host + try: + COMPUTERS.append(Computer(name, host, port)) + except (AssertionError, Exception): + return False + return True -def connect(host, port): - """Handle with connect function and logs if was successful""" - CONNECTION.connect(host, port) - _LOGGER.info(f"Connection to {CONNECTION.get_url()} established") +def find_computer(name): + for computer in COMPUTERS: + if computer.name == name: + return computer + return None def validate_response(response): @@ -74,63 +98,57 @@ def validate_response(response): raise ConnectionError() -def call_remote(id, action): - try: - CONNECTION.exe_remote(id, action) - _LOGGER.debug(f'Call -> Remote ID: "{id}"; Action: "{action}"') - # Log if request fails. - except ConnectionError: - _LOGGER.warning("Unable to call remote. Host is off") - - def setup(hass, config): """Setting up Unified Remote Integration""" # Fetching configuration entries. - host = config[DOMAIN].get(CONF_HOST) - port = config[DOMAIN].get(CONF_PORT) + hosts = config[DOMAIN].get(CONF_HOSTS) retry_delay = config[DOMAIN].get(CONF_RETRY) if retry_delay > 120: retry_delay = 120 - try: - # Establishing connection with host client. - connect(host, port) - # Handling with malformed url error. - except AssertionError as url_error: - _LOGGER.error(str(url_error)) - return False - except ConnectionError: - _LOGGER.warning( - "At the first moment host seems down, but the connection will be retried." - ) - except Exception as e: - _LOGGER.error(str(e)) + if not init_computers(hosts): return False def keep_alive(call): """Keep host listening our requests""" - try: - response = CONNECTION.exe_remote("", "") - _LOGGER.debug("Keep alive packet sent") - _LOGGER.debug( - f"Keep alive packet response: {response.content.decode('ascii')}" - ) - validate_response(response) - # If there's an connection error, try to reconnect. - except ConnectionError: + for computer in COMPUTERS: try: - _LOGGER.debug(f"Trying to reconnect with {host}") - connect(host, port) - except Exception as error: + response = computer.connection.exe_remote("", "") + _LOGGER.debug("Keep alive packet sent") _LOGGER.debug( - f"Unable to connect with {host}. Headers: {CONNECTION.get_headers()}" + f"Keep alive packet response: {response.content.decode('ascii')}" ) - _LOGGER.debug(f"Error: {error}") - pass + validate_response(response) + # If there's an connection error, try to reconnect. + except ConnectionError: + try: + _LOGGER.debug(f"Trying to reconnect with {computer.host}") + computer.connect() + except Exception as error: + computer.is_available = False + _LOGGER.info(f"The computer {computer.name} is now unavailable") + _LOGGER.debug( + f"Unable to connect with {computer.host}. Headers: {computer.connection.get_headers()}" + ) + _LOGGER.debug(f"Error: {error}") + pass def handle_call(call): """Handle the service call.""" # Fetch service data. + target = remote_name = call.data.get("target") + if target is None or target.strip() == "": + computer = COMPUTERS[0] + else: + computer = find_computer(target) + + if computer is None: + _LOGGER.error(f"No such computer called {target}") + return None + + if not computer.is_available: + _LOGGER.error(f"Unable to call remote. {target} is unavailable.") + remote_name = call.data.get("remote", DEFAULT_NAME) remote_id = call.data.get("remote_id", DEFAULT_NAME) action = call.data.get("action", DEFAULT_NAME) @@ -138,7 +156,7 @@ def handle_call(call): # Allows user to pass remote id without declaring it on remotes.yml if remote_id is not None: if not (remote_id == "" or action == ""): - call_remote(remote_id, action) + computer.call_remote(remote_id, action) return None # Check if none or empty service data was parsed. @@ -154,7 +172,7 @@ def handle_call(call): remote_id = remote["id"] # Check if given action exists in remote control list. if action in remote["controls"]: - call_remote(remote_id, action) + computer.call_remote(remote_id, action) else: # Log if called remote doens't exists on remotes.yml. _LOGGER.warning( diff --git a/custom_components/unified_remote/cli/computer.py b/custom_components/unified_remote/cli/computer.py new file mode 100644 index 0000000..caacd92 --- /dev/null +++ b/custom_components/unified_remote/cli/computer.py @@ -0,0 +1,45 @@ +import logging as log + +import requests + +from custom_components.unified_remote.cli.connection import Connection + +_LOGGER = log.getLogger(__name__) + + +class Computer: + def connect(self): + """Handle with connect function and logs if was successful""" + self.connection.connect(self.host, self.port) + _LOGGER.info(f"Connection to {self.name} established") + + def __init__(self, name: str, host: str, port: int): + self.name = name + self.host = host + self.port = port + self.is_available = False + self.connection = Connection() + try: + self.connect() + self.is_available = True + except AssertionError as url_error: + _LOGGER.error(str(url_error)) + raise + except requests.ConnectionError: + _LOGGER.warning( + f"At the first moment {name} seems down, but the connection will be retried." + ) + except Exception as e: + _LOGGER.error(str(e)) + raise + + def call_remote(self, id, action): + if not self.is_available: + _LOGGER.error(f"Unable to call remote. {self.name} is unavailable.") + return None + try: + self.connection.exe_remote(id, action) + _LOGGER.debug(f'Call -> Remote ID: "{id}"; Action: "{action}"') + # Log if request fails. + except requests.ConnectionError: + _LOGGER.error(f"Unable to call remote. {self.name} is unavailable.") diff --git a/custom_components/unified_remote/services.yaml b/custom_components/unified_remote/services.yaml index c51e33b..adacb1c 100644 --- a/custom_components/unified_remote/services.yaml +++ b/custom_components/unified_remote/services.yaml @@ -1,6 +1,9 @@ call: description: "Trigger remotes on target host using Unified Remote web client" fields: + target: + description: "Computer name to receive commands. Empty value means the first computer registered." + example: "" remote: description: "Remote name defined on remotes.yml file" example: "prime_video" diff --git a/custom_components/unified_remote/switch.py b/custom_components/unified_remote/switch.py deleted file mode 100644 index b0717aa..0000000 --- a/custom_components/unified_remote/switch.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Unified Remote switch platform""" -import logging - -import homeassistant.helpers.config_validation as cv -import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import (SERVICE_TOGGLE, SERVICE_TURN_OFF, - SERVICE_TURN_ON) - -_LOGGER = logging.getLogger(__name__) - -# Additional consts declarations -REMOTE_NAME = "remote" -REMOTE_ID = "remote_id" -REMOTE_ACTION = "action" - -# Remote config entry definition. -REMOTE_CONFIG = vol.Schema( - { - vol.Required(REMOTE_NAME, default=""): cv.string, - vol.Optional(REMOTE_ID): cv.string, - vol.Required(REMOTE_ACTION, default=""): cv.string, - } -) - -# Validation of the user's configuration. -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required("name"): cv.string, - vol.Required(SERVICE_TURN_ON, default=None): REMOTE_CONFIG, - vol.Optional(SERVICE_TURN_OFF): REMOTE_CONFIG, - vol.Optional(SERVICE_TOGGLE): REMOTE_CONFIG, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Unified Remote switch platform.""" - - # Set config entries to be parsed to switch entity. - name = config["name"] - remotes = { - SERVICE_TURN_ON: config.get(SERVICE_TURN_ON), - SERVICE_TURN_OFF: config.get(SERVICE_TURN_OFF), - SERVICE_TOGGLE: config.get(SERVICE_TOGGLE), - } - - # Add devices. - add_entities([UnifiedSwitch(hass, name, remotes)]) - - -class UnifiedSwitch(SwitchEntity): - "A switch that can calls remotes of Unified Remote client." - "It uses unified_remote.call service to do the job." - - def __init__(self, hass, name, remotes): - self._switch_name = name - self._remotes = remotes - self.hass = hass - self.call = self.hass.services.call - self._state = False - - def turn_on(self) -> None: - """Turn the entity on.""" - remote = self._remotes.get(SERVICE_TURN_ON) - if remote is not None: - self.call(domain="unified_remote", service="call", service_data=remote) - self._state = True - - def turn_off(self): - """Turn the entity off.""" - remote = self._remotes.get(SERVICE_TURN_OFF) - if remote is not None: - self.call(domain="unified_remote", service="call", service_data=remote) - self._state = False - - def toggle(self): - """Toggle the entity.""" - remote = self._remotes.get(SERVICE_TOGGLE) - if remote is not None: - self.call(domain="unified_remote", service="call", service_data=remote) - self._state = not self._state - - @property - def name(self): - return self._switch_name - - @property - def is_on(self): - return self._state