From 5251bc1a62620982559a215c8de34782e2ae931a Mon Sep 17 00:00:00 2001 From: Olivier <16240457+TiTidom-RC@users.noreply.github.com> Date: Sat, 23 May 2026 13:53:38 +0200 Subject: [PATCH 1/3] Add PHP/Python CI workflows and config Add GitHub Actions workflows to improve CI coverage: a PHP compatibility workflow (checkPHPCompat.yml) that runs PHPStan across PHP 7.4/8.2/8.4 against a sparse clone of Jeedom Core, and a Python lint workflow (checkPython.yml) using ruff for resources/tvremoted. Also add phpstan.neon.dist to enable reportUnmatchedIgnoredErrors, and tweak existing checkPHP.yml to name the PHP lint job. Update .vscode/settings.json to include phpstan and shivammathur entries. --- .github/workflows/checkPHP.yml | 1 + .github/workflows/checkPHPCompat.yml | 40 ++++++++++++++++++++++++++++ .github/workflows/checkPython.yml | 25 +++++++++++++++++ .vscode/settings.json | 2 ++ phpstan.neon.dist | 2 ++ 5 files changed, 70 insertions(+) create mode 100644 .github/workflows/checkPHPCompat.yml create mode 100644 .github/workflows/checkPython.yml create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/checkPHP.yml b/.github/workflows/checkPHP.yml index 0d68f3e..eae9699 100644 --- a/.github/workflows/checkPHP.yml +++ b/.github/workflows/checkPHP.yml @@ -7,6 +7,7 @@ on: jobs: php-lint: + name: PHP Lint — PHP 8.4 runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/checkPHPCompat.yml b/.github/workflows/checkPHPCompat.yml new file mode 100644 index 0000000..6c9e618 --- /dev/null +++ b/.github/workflows/checkPHPCompat.yml @@ -0,0 +1,40 @@ +name: PHP Compatibility Check + +on: + pull_request: + branches: + - beta + +jobs: + php-compat: + name: PHPStan — PHP ${{ matrix.php-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ['7.4', '8.2', '8.4'] + + steps: + - uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer + coverage: none + + - name: Install PHPStan + run: composer global require phpstan/phpstan --no-interaction --quiet + + - name: Checkout Jeedom Core (sparse) + run: | + git clone --depth 1 --filter=blob:none --sparse \ + https://github.com/jeedom/core.git /tmp/jeedom-core + git -C /tmp/jeedom-core sparse-checkout set core/class core/php core/repo + + - name: PHP ${{ matrix.php-version }} Compatibility Check + run: | + PHPSTAN_VERSION=$(echo "${{ matrix.php-version }}" | awk -F. '{printf "%d%02d00", $1, $2}') + printf 'includes:\n - phpstan.neon.dist\nparameters:\n phpVersion: %s\n scanDirectories:\n - /tmp/jeedom-core/core/class\n - /tmp/jeedom-core/core/php\n - /tmp/jeedom-core/core/repo\n' "$PHPSTAN_VERSION" > phpstan.neon + phpstan analyse --level 0 --no-progress core/ desktop/ plugin_info/ diff --git a/.github/workflows/checkPython.yml b/.github/workflows/checkPython.yml new file mode 100644 index 0000000..75c4024 --- /dev/null +++ b/.github/workflows/checkPython.yml @@ -0,0 +1,25 @@ +name: Python Check + +on: + pull_request: + branches: + - beta + +jobs: + python-check: + name: Ruff Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install ruff + run: pip install ruff + + - name: Ruff Check + run: ruff check resources/tvremoted/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 902281d..c9a2cd0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -60,6 +60,7 @@ "orangetv", "owtv", "paircode", + "phpstan", "phpunit", "plex", "primevideo", @@ -77,6 +78,7 @@ "scanupdtvremote", "screencap", "sendpaircode", + "shivammathur", "sockethost", "spotify", "stype", diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..4763a9e --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,2 @@ +parameters: + reportUnmatchedIgnoredErrors: true From 4112c2fb29d7ff7906771c85d7e853ca30569f0d Mon Sep 17 00:00:00 2001 From: Olivier <16240457+TiTidom-RC@users.noreply.github.com> Date: Wed, 27 May 2026 22:35:00 +0200 Subject: [PATCH 2/3] Add TV libraries log level option Introduce a configurable log level for TV-related libraries (zeroconf, androidtvremote2, adb_shell). Adds a tvLogLevel UI setting (default 'daemon' to inherit the daemon level), persists the default on install/update, passes --tvloglevel to the daemon, and applies the chosen level to the respective library loggers. Also bumps plugin version to 1.3.19 and updates zeroconf to 0.149.16. --- .vscode/settings.json | 1 + core/class/tvremote.class.php | 1 + plugin_info/configuration.php | 16 ++++++++++++++++ plugin_info/info.json | 2 +- plugin_info/install.php | 6 ++++++ resources/requirements.txt | 2 +- resources/tvremoted/config.py | 4 ++++ resources/tvremoted/tvremoted.py | 7 +++++++ 8 files changed, 37 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c9a2cd0..600a248 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -85,6 +85,7 @@ "tunein", "tvhosts", "TVHOSTS", + "tvloglevel", "tvremote", "tvremoted", "updatelasttime", diff --git a/core/class/tvremote.class.php b/core/class/tvremote.class.php index 3e1c9fd..ed1e85b 100644 --- a/core/class/tvremote.class.php +++ b/core/class/tvremote.class.php @@ -137,6 +137,7 @@ public static function deamon_start() { $path = realpath(__DIR__ . '/../../resources/tvremoted'); $cmd = self::PYTHON3_PATH . " {$path}/tvremoted.py"; $cmd .= ' --loglevel ' . log::convertLogLevel(log::getLogLevel(__CLASS__)); + $cmd .= ' --tvloglevel ' . config::byKey('tvLogLevel', __CLASS__, 'daemon'); $cmd .= ' --pluginversion ' . config::byKey('pluginVersion', __CLASS__, '0.0.0'); $cmd .= ' --socketport ' . config::byKey('socketport', __CLASS__, '55112'); $cmd .= ' --cyclefactor ' . config::byKey('cyclefactor', __CLASS__, '1.0'); diff --git a/plugin_info/configuration.php b/plugin_info/configuration.php index ae3a325..683bd55 100644 --- a/plugin_info/configuration.php +++ b/plugin_info/configuration.php @@ -113,6 +113,22 @@ +
+ +
+ +
+
diff --git a/plugin_info/info.json b/plugin_info/info.json index 71f20ac..095db41 100644 --- a/plugin_info/info.json +++ b/plugin_info/info.json @@ -1,7 +1,7 @@ { "id": "tvremote", "name": "TV Remote", - "pluginVersion": "1.3.18", + "pluginVersion": "1.3.19", "description": { "fr_FR": "Plugin pour contrôler les équipements Android de type Télévision (Sony, Nvidia Shield, Freebox TV, etc...) via le service de télécommande Google (TVRemote) ou via ADB. Il permet de contrôler les différentes fonctions de sa télévision (power, volume, entrées HDMI, lancement d'applications, chaînes TV, navigation dans l'interface, etc...)", "en_US": "Plugin to control Android TV devices (Sony, Nvidia Shield, Freebox TV, etc...) via Google remote control service (TVRemote) or via ADB. It allows you to control various functions of your TV (power, volume, HDMI inputs, launching applications, TV channels, navigating the interface, etc...)", diff --git a/plugin_info/install.php b/plugin_info/install.php index d544fae..2916daa 100644 --- a/plugin_info/install.php +++ b/plugin_info/install.php @@ -40,6 +40,9 @@ function tvremote_install() { if (config::byKey('cyclefactor', 'tvremote') == '') { config::save('cyclefactor', '1.0', 'tvremote'); } + if (config::byKey('tvLogLevel', 'tvremote') == '') { + config::save('tvLogLevel', 'daemon', 'tvremote'); + } if (config::byKey('debugInstallUpdates', 'tvremote') == '') { config::save('debugInstallUpdates', '0', 'tvremote'); } @@ -90,6 +93,9 @@ function tvremote_update() { if (config::byKey('cyclefactor', 'tvremote') == '') { config::save('cyclefactor', '1.0', 'tvremote'); } + if (config::byKey('tvLogLevel', 'tvremote') == '') { + config::save('tvLogLevel', 'daemon', 'tvremote'); + } if (config::byKey('debugInstallUpdates', 'tvremote') == '') { config::save('debugInstallUpdates', '0', 'tvremote'); } diff --git a/resources/requirements.txt b/resources/requirements.txt index 121a58c..1430afa 100644 --- a/resources/requirements.txt +++ b/resources/requirements.txt @@ -1,4 +1,4 @@ -zeroconf==0.148.0 +zeroconf==0.149.16 aiohttp==3.13.5 androidtvremote2==0.3.1 adb-shell[async]==0.4.4 diff --git a/resources/tvremoted/config.py b/resources/tvremoted/config.py index 8ccff8d..9b7ee9c 100644 --- a/resources/tvremoted/config.py +++ b/resources/tvremoted/config.py @@ -122,6 +122,10 @@ def socket_port(self): def log_level(self): return self._kwargs.get('loglevel', 'error') + @property + def tv_log_level(self): + return self._kwargs.get('tvloglevel', 'daemon') + @property def api_key(self): return self._kwargs.get('apikey', '') diff --git a/resources/tvremoted/tvremoted.py b/resources/tvremoted/tvremoted.py index 028cf2c..30f3c77 100644 --- a/resources/tvremoted/tvremoted.py +++ b/resources/tvremoted/tvremoted.py @@ -1557,6 +1557,7 @@ def close(self) -> None: def get_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description='TVRemote Daemon for Jeedom plugin') parser.add_argument("--loglevel", help="Log Level for the daemon", type=str) + parser.add_argument("--tvloglevel", help="Log Level for TV libraries (zeroconf, androidtvremote2, adb_shell)", type=str, default='daemon') parser.add_argument("--pluginversion", help="Plugin Version", type=str) parser.add_argument("--socketport", help="Port for TVRemote server", type=str) parser.add_argument("--cyclefactor", help="Cycle Factor", type=str) @@ -1591,12 +1592,18 @@ def shutdown() -> None: Utils.init_logger(config.log_level) _LOGGER = logging.getLogger(__name__) logging.getLogger('asyncio').setLevel(logging.WARNING) +if config.tv_log_level != 'daemon': + _tv_level = Utils.convert_log_level(config.tv_log_level) + logging.getLogger('zeroconf').setLevel(_tv_level) + logging.getLogger('androidtvremote2').setLevel(_tv_level) + logging.getLogger('adb_shell').setLevel(_tv_level) try: _LOGGER.info('[DAEMON] Starting Daemon') _LOGGER.info('[DAEMON] Plugin Version: %s', config.plugin_version) _LOGGER.info('[DAEMON] Pairing Name: %s', config.client_name) _LOGGER.info('[DAEMON] Log Level: %s', config.log_level) + _LOGGER.info('[DAEMON] TV Log Level: %s', config.tv_log_level) _LOGGER.info('[DAEMON] Socket Port: %s', config.socket_port) _LOGGER.info('[DAEMON] Socket Host: %s', config.socket_host) _LOGGER.info('[DAEMON] Cycle Factor: %s', config.cycle_factor) From 1adef96b2b8e7ed37f67ac72292b502c40895315 Mon Sep 17 00:00:00 2001 From: Olivier <16240457+TiTidom-RC@users.noreply.github.com> Date: Wed, 27 May 2026 22:56:12 +0200 Subject: [PATCH 3/3] Promote debug logs to info and fix pairing log Change several logger.debug calls to logger.info to surface important daemon events (EQRemote, EQRemoteADB, TVRemoted). Also fix a malformed pairing log format in sendBeginPairing (removed an extra placeholder) so the log arguments match the format string. These changes make scan/add/remove/pairing/close events more visible and prevent a potential logging formatting error. --- resources/tvremoted/tvremoted.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/resources/tvremoted/tvremoted.py b/resources/tvremoted/tvremoted.py index 30f3c77..0334c47 100644 --- a/resources/tvremoted/tvremoted.py +++ b/resources/tvremoted/tvremoted.py @@ -78,7 +78,7 @@ async def main(self) -> None: You should start the asyncio task with this function like this: `asyncio.createtask(myEQRemote.main())` """ try: - self._logger.debug("[EQRemote][MAIN][%s] Starting Main for Host :: %s", self._macAddr, self._host) + self._logger.info("[EQRemote][MAIN][%s] Starting Main for Host :: %s", self._macAddr, self._host) self._remote = AndroidTVRemote(self._config.client_name, self._config.cert_file, self._config.key_file, self._host, enable_ime=self._enable_ime) @@ -421,7 +421,7 @@ async def main(self) -> None: You should start the asyncio task with this function like this: `asyncio.create_task(myEQRemoteADB.main())` """ try: - self._logger.debug("[EQRemoteADB][MAIN][%s] Starting Main for Host :: %s", self._macAddr, self._host) + self._logger.info("[EQRemoteADB][MAIN][%s] Starting Main for Host :: %s", self._macAddr, self._host) # Small initial delay to let the network and device stabilize after daemon start await asyncio.sleep(2) @@ -952,28 +952,28 @@ async def _on_socket_message(self, message: dict) -> None: else: self._logger.warning('[DAEMON][SOCKET] Unknown Action :: %s', message['cmd_action']) elif message['cmd'] == "scanOn": - self._logger.debug('[DAEMON][SOCKET] ScanState = scanOn') + self._logger.info('[DAEMON][SOCKET] ScanState = scanOn') self._config.scanmode = True self._config.scanmode_start = int(time.time()) if self._jeedom_publisher is not None: await self._jeedom_publisher.send_to_jeedom({'scanState': 'scanOn'}) elif message['cmd'] == "scanOff": - self._logger.debug('[DAEMON][SOCKET] ScanState = scanOff') + self._logger.info('[DAEMON][SOCKET] ScanState = scanOff') self._config.scanmode = False if self._jeedom_publisher is not None: await self._jeedom_publisher.send_to_jeedom({'scanState': 'scanOff'}) elif message['cmd'] == "sendBeginPairing": - self._logger.debug('[DAEMON][SOCKET] Begin Pairing for (Mac :: %s) :: %s:%s / %s', message['mac'], message['host'], message['port']) + self._logger.info('[DAEMON][SOCKET] Begin Pairing for (Mac :: %s) :: %s:%s', message['mac'], message['host'], message['port']) await self._pairing(message['mac'], message['host'], message['port']) elif message['cmd'] == "sendBeginPairingAdb": - self._logger.debug('[DAEMON][SOCKET] Begin ADB Pairing for (Mac :: %s) :: %s', message['mac'], message['host']) + self._logger.info('[DAEMON][SOCKET] Begin ADB Pairing for (Mac :: %s) :: %s', message['mac'], message['host']) await self._pairing_adb(message['mac'], message['host']) elif message['cmd'] == "sendPairCode": self._logger.debug('[DAEMON][SOCKET] Received Pairing Code (Mac :: %s) :: %s', message['mac'], message['paircode']) self._config.pairing_code = message['paircode'] elif message['cmd'] == "addtvremote": if all(keys in message for keys in ('mac', 'host', 'port', 'friendly_name')): - self._logger.debug('[DAEMON][SOCKET] Add TVRemote Device (Mac :: %s) :: %s:%s', message['mac'], message['host'], message['port']) + self._logger.info('[DAEMON][SOCKET] Add TVRemote Device (Mac :: %s) :: %s:%s', message['mac'], message['host'], message['port']) if message['host'] not in self._config.known_hosts: self._config.known_hosts.append(message['host']) self._logger.debug('[DAEMON][SOCKET] Add TVRemote (AndroidTVRemote2) to KNOWN Devices :: %s', str(self._config.known_hosts)) @@ -996,7 +996,7 @@ async def _on_socket_message(self, message: dict) -> None: self._logger.debug('[DAEMON][SOCKET] TVRemote device %s already exists', message['mac']) elif message['cmd'] == "removetvremote": if all(keys in message for keys in ('mac', 'host', 'port', 'friendly_name')): - self._logger.debug('[DAEMON][SOCKET] Remove TVRemote (Mac :: %s) :: %s:%s', message['mac'], message['host'], message['port']) + self._logger.info('[DAEMON][SOCKET] Remove TVRemote (Mac :: %s) :: %s:%s', message['mac'], message['host'], message['port']) if message['host'] in self._config.known_hosts: self._config.known_hosts.remove(message['host']) self._logger.debug('[DAEMON][SOCKET] Remove TVRemote (AndroidTVRemote2) from KNOWN Devices :: %s', str(self._config.known_hosts)) @@ -1020,7 +1020,7 @@ async def _on_socket_message(self, message: dict) -> None: del self._config.remote_devices[message['mac']] elif message['cmd'] == "addtvremote_adb": if all(keys in message for keys in ('mac', 'host', 'friendly_name')): - self._logger.debug('[DAEMON][SOCKET] Add ADB Device (Mac :: %s) :: %s', message['mac'], message['host']) + self._logger.info('[DAEMON][SOCKET] Add ADB Device (Mac :: %s) :: %s', message['mac'], message['host']) # Ensure ADB keys exist before adding device await self.ensure_adb_keys(notify_jeedom=False) if message['host'] not in self._config.known_hosts_adb: @@ -1063,7 +1063,7 @@ async def _on_socket_message(self, message: dict) -> None: self._logger.debug('[DAEMON][SOCKET] Device %s already exists, updated paired=%s, persistent=%s', message['mac'], device._adb_paired, device._persistent_connection) elif message['cmd'] == "removetvremote_adb": if all(keys in message for keys in ('mac', 'host', 'friendly_name')): - self._logger.debug('[DAEMON][SOCKET] Remove ADB Device (Mac :: %s) :: %s', message['mac'], message['host']) + self._logger.info('[DAEMON][SOCKET] Remove ADB Device (Mac :: %s) :: %s', message['mac'], message['host']) if message['host'] in self._config.known_hosts_adb: self._config.known_hosts_adb.remove(message['host']) self._logger.debug('[DAEMON][SOCKET] Remove ADB from KNOWN Devices :: %s', str(self._config.known_hosts_adb)) @@ -1118,7 +1118,7 @@ async def _pairing(self, _mac=None, _host=None, _port=None) -> None: try: try: - self._logger.debug("[PAIRING][START][%s] Start Pairing...", _mac) + self._logger.info("[PAIRING][START][%s] Start Pairing...", _mac) await remote.async_start_pairing() except CannotConnect as e: self._logger.warning("[PAIRING][START][%s] Cannot connect (device may be offline) :: %s", _mac, e) @@ -1517,7 +1517,7 @@ def close(self) -> None: This function can be called from outside to stop the daemon if needed` You need to close your remote connexions and cancel background tasks if any here. """ - self._logger.debug('[CLOSE] Cancelling all tasks') + self._logger.info('[CLOSE] Cancelling all tasks') # Collect all tasks to cancel tasks_to_cancel = []