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..600a248 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -60,6 +60,7 @@ "orangetv", "owtv", "paircode", + "phpstan", "phpunit", "plex", "primevideo", @@ -77,12 +78,14 @@ "scanupdtvremote", "screencap", "sendpaircode", + "shivammathur", "sockethost", "spotify", "stype", "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/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 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..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 = [] @@ -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)