From 0cd8b54edc09e5d71f4217a73727ef6cf04f281d Mon Sep 17 00:00:00 2001 From: albaintor <118518828+albaintor@users.noreply.github.com> Date: Sat, 13 Jul 2024 12:29:32 +0200 Subject: [PATCH] Reformat code --- .github/ISSUE_TEMPLATE/bug_report.yml | 53 ++++ .github/ISSUE_TEMPLATE/config.yml | 11 + .github/ISSUE_TEMPLATE/feature_request.yml | 25 ++ .github/workflows/build.yml | 146 ++++++++++ .github/workflows/python-code-format.yml | 52 ++++ intg-zidoo/config.py | 2 + intg-zidoo/const.py | 10 +- intg-zidoo/discover.py | 34 ++- intg-zidoo/driver.py | 69 +++-- intg-zidoo/media_player.py | 78 ++--- intg-zidoo/setup_flow.py | 66 ++++- intg-zidoo/test.py | 48 --- intg-zidoo/zidooaio.py | 321 +++++++++++++-------- requirements.txt | 6 +- 14 files changed, 664 insertions(+), 257 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/python-code-format.yml delete mode 100644 intg-zidoo/test.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..4502d23 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,53 @@ +name: Bug report +description: Create a report to help us improve +title: '' +labels: [bug] + +body: + - type: markdown + attributes: + value: | + Please check the existing issues first to see if the bug has already been recorded. + If you have more information about an existing bug, please add it as a comment and don't open a new issue. + Thank you! + - type: textarea + attributes: + label: Description + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + attributes: + label: How to Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. ... + 2. ... + 3. See error + validations: + required: true + - type: textarea + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: input + id: intg_version + attributes: + label: Integration version + description: You can find the integration version in driver.json or in the UI under Settings/Integrations + placeholder: ex. v0.4.5 + validations: + required: false + - type: textarea + attributes: + label: Additional context + description: | + Add any other context about the problem here. Otherwise you can ignore this section. + How has this issue affected you? What are you trying to accomplish? + Providing context helps us come up with a solution that is most useful in the real world + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..29a982a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Unfolded Circle Community Forum + url: https://unfolded.community/ + about: Please ask and answer questions here. + - name: Unfolded Circle Discord Channel + url: https://unfolded.chat/ + about: Chat with the community and for asking questions. + - name: Unfolded Circle Contact Form + url: https://unfoldedcircle.com/contact + about: Write us a message on our website. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..a9ae273 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,25 @@ +name: Feature request +description: Suggest an idea +title: '<title>' +labels: [enhancement] + +body: + - type: markdown + attributes: + value: | + Please check the existing issues first to see if your feature request has already been recorded. + - type: textarea + attributes: + label: Description + description: A clear and concise description of what the feature request is about. + validations: + required: true + - type: textarea + attributes: + label: Additional context + description: | + Add any other context or screenshots about the feature request here. + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f8b7ed0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,146 @@ +# GitHub Action to build a self-contained binary of the Android TV Python driver +--- +name: "Build & Release" + +on: + push: + branches: [main] + tags: + - v[0-9]+.[0-9]+.[0-9]+* + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + +env: + INTG_NAME: zidoo + HASH_FILENAME: uc-intg-zidoo.hash + # Python version to use in the builder image. See https://hub.docker.com/r/unfoldedcircle/r2-pyinstaller for possible versions. + PYTHON_VER: 3.11.6-0.2.0 + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # History of 200 should be more than enough to calculate commit count since last release tag. + fetch-depth: 200 + + - name: Fetch all tags to determine version + run: | + git fetch origin +refs/tags/*:refs/tags/* + echo "VERSION=$(git describe --match "v[0-9]*" --tags HEAD --always)" >> $GITHUB_ENV + + - name: Verify driver.json version for release build + if: contains(github.ref, 'tags/v') + run: | + DRIVER_VERSION="v$(jq .version -r driver.json)" + if [ "${{ env.VERSION }}" != "$DRIVER_VERSION" ]; then + echo "Version in driver.json ($DRIVER_VERSION) doesn't match git version tag (${{ env.VERSION }})!" + exit 1 + fi + + - name: Prepare + run: | + sudo apt-get update && sudo apt-get install -y qemu binfmt-support qemu-user-static + docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + echo "Starting pyinstaller build" + docker run --rm --name builder \ + --platform=aarch64 \ + --user=$(id -u):$(id -g) \ + -v ${GITHUB_WORKSPACE}:/workspace \ + docker.io/unfoldedcircle/r2-pyinstaller:${PYTHON_VER} \ + bash -c \ + "cd /workspace && \ + python -m pip install -r requirements.txt && \ + pyinstaller --clean --onefile --name intg-zidoo intg-zidoo/driver.py" + + - name: Add version + run: | + mkdir -p artifacts + cd artifacts + echo ${{ env.VERSION }} > version.txt + + - name: Prepare artifacts + shell: bash + run: | + cp dist/intg-zidoo artifacts/ + cp driver.json artifacts/ + cp orange.png artifacts/ + echo "ARTIFACT_NAME=uc-intg-${{ env.INTG_NAME }}-${{ env.VERSION }}-aarch64" >> $GITHUB_ENV + + - name: Create upload artifact + shell: bash + run: | + tar czvf ${{ env.ARTIFACT_NAME }}.tar.gz -C ${GITHUB_WORKSPACE}/artifacts . + ls -lah + + - uses: actions/upload-artifact@v4 + id: upload_artifact + with: + name: ${{ env.ARTIFACT_NAME }} + path: ${{ env.ARTIFACT_NAME }}.tar.gz + if-no-files-found: error + retention-days: 3 + + release: + name: Create Release + if: github.ref == 'refs/heads/main' || contains(github.ref, 'tags/v') + runs-on: ubuntu-latest + needs: [build] + + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + + - name: Extract build archives from downloaded files + run: | + ls -R + # extract tar.gz build archives from downloaded artifacts + # (wrapped in tar from actions/upload-artifact, then extracted into a directory by actions/download-artifact) + for D in * + do if [ -d "${D}" ]; then + mv $D/* ./ + fi + done; + + # Use a common timestamp for all matrix build artifacts + - name: Get timestamp + run: | + echo "TIMESTAMP=$(date +"%Y%m%d_%H%M%S")" >> $GITHUB_ENV + + # Add timestamp to development builds + - name: Create GitHub development build archives + if: "!contains(github.ref, 'tags/v')" + run: | + # append timestamp + for filename in *.tar.gz; do mv $filename "$(basename $filename .tar.gz)-${{ env.TIMESTAMP }}.tar.gz"; done; + for filename in *.tar.gz; do echo "sha256 `sha256sum $filename`" >> ${{ env.HASH_FILENAME }}; done; + + - name: Create Pre-Release + uses: "marvinpinto/action-automatic-releases@latest" + if: "!contains(github.ref, 'tags/v')" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: "latest" + prerelease: true + title: "Development Build" + files: | + *.tar.gz + ${{ env.HASH_FILENAME }} + + - name: Create GitHub release archives + if: "contains(github.ref, 'tags/v')" + run: | + for filename in *.tar.gz; do echo "sha256 `sha256sum $filename`" >> ${{ env.HASH_FILENAME }}; done; + + - name: Create Release + uses: "marvinpinto/action-automatic-releases@latest" + if: "contains(github.ref, 'tags/v')" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + prerelease: false + files: | + *.tar.gz + ${{ env.HASH_FILENAME }} diff --git a/.github/workflows/python-code-format.yml b/.github/workflows/python-code-format.yml new file mode 100644 index 0000000..c14acab --- /dev/null +++ b/.github/workflows/python-code-format.yml @@ -0,0 +1,52 @@ +name: Check Python code formatting + +on: + push: + paths: + - 'intg-zidoo/**' + - 'requirements.txt' + - 'test-requirements.txt' + - 'tests/**' + - '.github/**/*.yml' + - '.pylintrc' + - 'pyproject.toml' + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-22.04 + + name: Check Python code formatting + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install pip + run: | + python -m pip install --upgrade pip + + - name: Install dependencies + run: | + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi + - name: Analyzing the code with pylint + run: | + python -m pylint intg-zidoo + - name: Lint with flake8 + run: | + python -m flake8 intg-zidoo --count --show-source --statistics + - name: Check code formatting with isort + run: | + python -m isort intg-zidoo/. --check --verbose + - name: Check code formatting with black + run: | + python -m black intg-zidoo --check --diff --verbose --line-length 120 diff --git a/intg-zidoo/config.py b/intg-zidoo/config.py index 16f104a..d9bfb7c 100644 --- a/intg-zidoo/config.py +++ b/intg-zidoo/config.py @@ -40,6 +40,7 @@ def device_from_entity_id(entity_id: str) -> str | None: class DeviceInstance: """Orange TV device configuration.""" + # pylint: disable = W0622 id: str name: str address: str @@ -47,6 +48,7 @@ class DeviceInstance: wifi_mac_address: str def __init__(self, id, name, address, net_mac_address=None, wifi_mac_address=None): + """Initialize device instance config.""" self.id = id self.name = name self.address = address diff --git a/intg-zidoo/const.py b/intg-zidoo/const.py index 2b0de78..13aa59b 100644 --- a/intg-zidoo/const.py +++ b/intg-zidoo/const.py @@ -58,7 +58,15 @@ 1: MediaType.MUSIC, # music 2: MediaType.VIDEO, # video # 3: MediaType.IMAGE, # image - # 4: 'text', # 5: 'apk', # 6: 'pdf', # 7: 'document', # 8: 'spreadsheet', # 9: 'presentation', # 10: 'web', # 11: 'archive' , # 12: 'other' + # 4: 'text', + # 5: 'apk', + # 6: 'pdf', + # 7: 'document', + # 8: 'spreadsheet', + # 9: 'presentation', + # 10: 'web', + # 11: 'archive' , + # 12: 'other' 1000: MEDIA_TYPE_FILE, # hhd 1001: MEDIA_TYPE_FILE, # usb 1002: MEDIA_TYPE_FILE, # usb diff --git a/intg-zidoo/discover.py b/intg-zidoo/discover.py index 1a0c066..c1c9672 100644 --- a/intg-zidoo/discover.py +++ b/intg-zidoo/discover.py @@ -1,9 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -This module implements a discovery function for Orange TV. - -""" +"""This module implements a discovery function for Orange TV.""" import asyncio import json @@ -124,9 +121,9 @@ async def async_send_ssdp_broadcast() -> Set[str]: _LOGGER.debug("Following devices found: %s", urls) return urls + async def async_send_ssdp_broadcast_ip(ip_addr: str) -> Set[str]: """Send SSDP broadcast messages to a single IP.""" - try: # Ignore 169.254.0.0/16 addresses if ip_addr.startswith("169.254."): @@ -138,7 +135,9 @@ async def async_send_ssdp_broadcast_ip(ip_addr: str) -> Set[str]: # Get asyncio loop loop = asyncio.get_event_loop() - transport, protocol = await loop.create_datagram_endpoint(OrangeTVSSDP, sock=sock) + transport, protocol = await loop.create_datagram_endpoint( + OrangeTVSSDP, sock=sock + ) # Wait for the timeout period await asyncio.sleep(SSDP_MX) @@ -146,9 +145,12 @@ async def async_send_ssdp_broadcast_ip(ip_addr: str) -> Set[str]: # Close the connection transport.close() - _LOGGER.debug("Got %s results after SSDP queries using ip %s", len(protocol.urls), ip_addr) + _LOGGER.debug( + "Got %s results after SSDP queries using ip %s", len(protocol.urls), ip_addr + ) return protocol.urls + # pylint: disable = W0718 except Exception: return set() @@ -177,15 +179,23 @@ def evaluate_scpd_xml(url: str, body: str) -> Optional[Dict]: device_xml = root.find(SCPD_DEVICE) elif root.find(SCPD_DEVICE).find(SCPD_DEVICELIST) is not None: for dev in root.find(SCPD_DEVICE).find(SCPD_DEVICELIST): - if dev.find(SCPD_DEVICETYPE).text in SUPPORTED_DEVICETYPES and dev.find(SCPD_SERIALNUMBER) is not None: + if ( + dev.find(SCPD_DEVICETYPE).text in SUPPORTED_DEVICETYPES + and dev.find(SCPD_SERIALNUMBER) is not None + ): device_xml = dev break if device_xml is None: return None - if device_xml.find(SCPD_PRESENTATIONURL) is not None and device_xml.find(SCPD_PRESENTATIONURL).text != "/": - device["host"] = urlparse(device_xml.find(SCPD_PRESENTATIONURL).text).hostname + if ( + device_xml.find(SCPD_PRESENTATIONURL) is not None + and device_xml.find(SCPD_PRESENTATIONURL).text != "/" + ): + device["host"] = urlparse( + device_xml.find(SCPD_PRESENTATIONURL).text + ).hostname device["presentationURL"] = device_xml.find(SCPD_PRESENTATIONURL).text else: device["host"] = urlparse(url).hostname @@ -201,7 +211,9 @@ def evaluate_scpd_xml(url: str, body: str) -> Optional[Dict]: ParseError, UnicodeDecodeError, ) as err: - _LOGGER.error("Error occurred during evaluation of SCPD XML from URI %s: %s", url, err) + _LOGGER.error( + "Error occurred during evaluation of SCPD XML from URI %s: %s", url, err + ) return None diff --git a/intg-zidoo/driver.py b/intg-zidoo/driver.py index 30b977c..3f8ced5 100644 --- a/intg-zidoo/driver.py +++ b/intg-zidoo/driver.py @@ -12,20 +12,18 @@ import os from typing import Any -import websockets -from ucapi.api import filter_log_msg_data, IntegrationAPI - import config import media_player import setup_flow import ucapi - +import ucapi.api_definitions as uc +import websockets import zidooaio from config import device_from_entity_id -from ucapi.media_player import Attributes as MediaAttr, States - +from ucapi.api import IntegrationAPI, filter_log_msg_data +from ucapi.media_player import Attributes as MediaAttr +from ucapi.media_player import States from zidooaio import ZidooRC -import ucapi.api_definitions as uc _LOG = logging.getLogger("driver") # avoid having __main__ in log messages _LOOP = asyncio.get_event_loop() @@ -76,8 +74,8 @@ async def on_r2_disconnect_cmd(): @api.listens_to(ucapi.Events.ENTER_STANDBY) async def on_r2_enter_standby() -> None: - """ - Enter standby notification from Remote Two. + """Enter standby notification from Remote Two. + Disconnect every ZidooTV instances. """ global _R2_IN_STANDBY @@ -121,14 +119,19 @@ async def on_subscribe_entities(entity_ids: list[str]) -> None: if device_id in _configured_devices: device = _configured_devices[device_id] state = media_player.state_from_device(device.state) - api.configured_entities.update_attributes(entity_id, {ucapi.media_player.Attributes.STATE: state}) + api.configured_entities.update_attributes( + entity_id, {ucapi.media_player.Attributes.STATE: state} + ) continue device = config.devices.get(device_id) if device: _configure_new_device(device, connect=True) else: - _LOG.error("Failed to subscribe entity %s: no device configuration found", entity_id) + _LOG.error( + "Failed to subscribe entity %s: no device configuration found", + entity_id, + ) @api.listens_to(ucapi.Events.UNSUBSCRIBE_ENTITIES) @@ -173,7 +176,10 @@ async def on_device_connected(device_id: str): ): # TODO why STANDBY? api.configured_entities.update_attributes( - entity_id, {ucapi.media_player.Attributes.STATE: ucapi.media_player.States.STANDBY} + entity_id, + { + ucapi.media_player.Attributes.STATE: ucapi.media_player.States.STANDBY + }, ) @@ -188,7 +194,10 @@ async def on_device_disconnected(avr_id: str): if configured_entity.entity_type == ucapi.EntityTypes.MEDIA_PLAYER: api.configured_entities.update_attributes( - entity_id, {ucapi.media_player.Attributes.STATE: ucapi.media_player.States.UNAVAILABLE} + entity_id, + { + ucapi.media_player.Attributes.STATE: ucapi.media_player.States.UNAVAILABLE + }, ) # TODO #20 when multiple devices are supported, the device state logic isn't that simple anymore! @@ -206,7 +215,10 @@ async def on_avr_connection_error(avr_id: str, message): if configured_entity.entity_type == ucapi.EntityTypes.MEDIA_PLAYER: api.configured_entities.update_attributes( - entity_id, {ucapi.media_player.Attributes.STATE: ucapi.media_player.States.UNAVAILABLE} + entity_id, + { + ucapi.media_player.Attributes.STATE: ucapi.media_player.States.UNAVAILABLE + }, ) # TODO #20 when multiple devices are supported, the device state logic isn't that simple anymore! @@ -217,7 +229,12 @@ async def handle_avr_address_change(avr_id: str, address: str) -> None: """Update device configuration with changed IP address.""" device = config.devices.get(avr_id) if device and device.address != address: - _LOG.info("Updating IP address of configured AVR %s: %s -> %s", avr_id, device.address, address) + _LOG.info( + "Updating IP address of configured AVR %s: %s -> %s", + avr_id, + device.address, + address, + ) device.address = address config.devices.update(device) @@ -275,7 +292,9 @@ def _entities_from_avr(avr_id: str) -> list[str]: return [f"media_player.{avr_id}"] -def _configure_new_device(device_config: config.DeviceInstance, connect: bool = True) -> None: +def _configure_new_device( + device_config: config.DeviceInstance, connect: bool = True +) -> None: """ Create and configure a new device. @@ -302,7 +321,9 @@ def _configure_new_device(device_config: config.DeviceInstance, connect: bool = _register_available_entities(device_config, device) -def _register_available_entities(config_device: config.DeviceInstance, device: ZidooRC) -> None: +def _register_available_entities( + config_device: config.DeviceInstance, device: ZidooRC +) -> None: """ Create entities for given receiver device and register them as available entities. @@ -326,7 +347,9 @@ def on_device_added(device: config.DeviceInstance) -> None: def on_device_removed(device: config.DeviceInstance | None) -> None: """Handle a removed device in the configuration.""" if device is None: - _LOG.debug("Configuration cleared, disconnecting & removing all configured AVR instances") + _LOG.debug( + "Configuration cleared, disconnecting & removing all configured AVR instances" + ) for configured in _configured_devices.values(): _LOOP.create_task(_async_remove(configured)) _configured_devices.clear() @@ -349,7 +372,7 @@ async def _async_remove(device: ZidooRC) -> None: async def patched_broadcast_ws_event( - self, msg: str, msg_data: dict[str, Any], category: uc.EventCategory + self, msg: str, msg_data: dict[str, Any], category: uc.EventCategory ) -> None: """ Send the given event-message to all connected WebSocket clients. @@ -367,6 +390,7 @@ async def patched_broadcast_ws_event( if _LOG.isEnabledFor(logging.DEBUG): data_log = json.dumps(data) if filter_log_msg_data(data) else data_dump + # pylint: disable = W0212 for websocket in self._clients.copy(): if _LOG.isEnabledFor(logging.DEBUG): _LOG.debug("[%s] ->: %s", websocket.remote_address, data_log) @@ -388,19 +412,22 @@ async def main(): logging.getLogger("receiver").setLevel(level) logging.getLogger("setup_flow").setLevel(level) - config.devices = config.Devices(api.config_dir_path, on_device_added, on_device_removed) + config.devices = config.Devices( + api.config_dir_path, on_device_added, on_device_removed + ) for device in config.devices.all(): _LOG.debug("UC Zidoo device %s %s", device.id, device.address) _configure_new_device(device, connect=False) # _LOOP.create_task(receiver_status_poller()) for device in _configured_devices.values(): - if device.state == States.OFF or device.state == States.UNKNOWN: + if device.state in [States.OFF, States.UNKNOWN]: continue _LOOP.create_task(device.async_update_data()) _LOOP.create_task(device_status_poller()) # Patched method + # pylint: disable = W0212 IntegrationAPI._broadcast_ws_event = patched_broadcast_ws_event await api.init("driver.json", setup_flow.driver_setup_handler) diff --git a/intg-zidoo/media_player.py b/intg-zidoo/media_player.py index 3300ea8..5c4af22 100644 --- a/intg-zidoo/media_player.py +++ b/intg-zidoo/media_player.py @@ -11,9 +11,16 @@ import zidooaio from config import DeviceInstance, create_entity_id from ucapi import EntityTypes, MediaPlayer, StatusCodes -from ucapi.media_player import Attributes, Commands, DeviceClasses, Features, States, MediaType, Options - -from zidooaio import ZidooRC, ZKEYS +from ucapi.media_player import ( + Attributes, + Commands, + DeviceClasses, + Features, + MediaType, + Options, + States, +) +from zidooaio import ZKEYS, ZidooRC _LOG = logging.getLogger(__name__) @@ -40,7 +47,7 @@ ZKEYS.ZKEY_REPEAT, ZKEYS.ZKEY_PICTURE_IN_PICTURE, ZKEYS.ZKEY_SELECT, - ZKEYS.ZKEY_LIGHT + ZKEYS.ZKEY_LIGHT, ] @@ -103,10 +110,12 @@ def __init__(self, config_device: DeviceInstance, device: ZidooRC): features, attributes, device_class=DeviceClasses.SET_TOP_BOX, - options={Options.SIMPLE_COMMANDS: SIMPLE_COMMANDS} + options={Options.SIMPLE_COMMANDS: SIMPLE_COMMANDS}, ) - async def command(self, cmd_id: str, params: dict[str, Any] | None = None) -> StatusCodes: + async def command( + self, cmd_id: str, params: dict[str, Any] | None = None + ) -> StatusCodes: """ Media-player entity command handler. @@ -116,6 +125,7 @@ async def command(self, cmd_id: str, params: dict[str, Any] | None = None) -> St :param params: optional command parameters :return: status code of the command request """ + # pylint: disable = R0915 _LOG.info("Got %s command request: %s %s", self.id, cmd_id, params) if self._device is None: @@ -142,65 +152,65 @@ async def command(self, cmd_id: str, params: dict[str, Any] | None = None) -> St elif cmd_id == Commands.PREVIOUS: res = await self._device.media_previous_track() elif cmd_id == Commands.CHANNEL_UP: - res = await self._device._send_key(ZKEYS.ZKEY_PAGE_UP) + res = await self._device.send_key(ZKEYS.ZKEY_PAGE_UP) elif cmd_id == Commands.CHANNEL_DOWN: - res = await self._device._send_key(ZKEYS.ZKEY_PAGE_DOWN) + res = await self._device.send_key(ZKEYS.ZKEY_PAGE_DOWN) elif cmd_id == Commands.PLAY_PAUSE: res = await self._device.media_play_pause() elif cmd_id == Commands.STOP: res = await self._device.media_stop() elif cmd_id == Commands.FAST_FORWARD: - res = await self._device._send_key(ZKEYS.ZKEY_MEDIA_FORWARDS) + res = await self._device.send_key(ZKEYS.ZKEY_MEDIA_FORWARDS) elif cmd_id == Commands.REWIND: - res = await self._device._send_key(ZKEYS.ZKEY_MEDIA_BACKWARDS) + res = await self._device.send_key(ZKEYS.ZKEY_MEDIA_BACKWARDS) elif cmd_id == Commands.RECORD: - res = await self._device._send_key(ZKEYS.ZKEY_RECORD) + res = await self._device.send_key(ZKEYS.ZKEY_RECORD) elif cmd_id == Commands.CURSOR_UP: - res = await self._device._send_key(ZKEYS.ZKEY_UP) + res = await self._device.send_key(ZKEYS.ZKEY_UP) elif cmd_id == Commands.CURSOR_DOWN: - res = await self._device._send_key(ZKEYS.ZKEY_DOWN) + res = await self._device.send_key(ZKEYS.ZKEY_DOWN) elif cmd_id == Commands.CURSOR_LEFT: - res = await self._device._send_key(ZKEYS.ZKEY_LEFT) + res = await self._device.send_key(ZKEYS.ZKEY_LEFT) elif cmd_id == Commands.CURSOR_RIGHT: - res = await self._device._send_key(ZKEYS.ZKEY_RIGHT) + res = await self._device.send_key(ZKEYS.ZKEY_RIGHT) elif cmd_id == Commands.CURSOR_ENTER: - res = await self._device._send_key(ZKEYS.ZKEY_OK) + res = await self._device.send_key(ZKEYS.ZKEY_OK) elif cmd_id == Commands.BACK: - res = await self._device._send_key(ZKEYS.ZKEY_BACK) + res = await self._device.send_key(ZKEYS.ZKEY_BACK) elif cmd_id == Commands.MENU: - res = await self._device._send_key(ZKEYS.ZKEY_MENU) + res = await self._device.send_key(ZKEYS.ZKEY_MENU) elif cmd_id == Commands.HOME: - res = await self._device._send_key(ZKEYS.ZKEY_HOME) + res = await self._device.send_key(ZKEYS.ZKEY_HOME) elif cmd_id == Commands.SETTINGS: - res = await self._device._send_key(ZKEYS.ZKEY_APP_SWITCH) + res = await self._device.send_key(ZKEYS.ZKEY_APP_SWITCH) elif cmd_id == Commands.CONTEXT_MENU: - res = await self._device._send_key(ZKEYS.ZKEY_POPUP_MENU) + res = await self._device.send_key(ZKEYS.ZKEY_POPUP_MENU) elif cmd_id == Commands.INFO: - res = await self._device._send_key(ZKEYS.ZKEY_INFO) + res = await self._device.send_key(ZKEYS.ZKEY_INFO) elif cmd_id == Commands.AUDIO_TRACK: - res = await self._device._send_key(ZKEYS.ZKEY_AUDIO) + res = await self._device.send_key(ZKEYS.ZKEY_AUDIO) elif cmd_id == Commands.SUBTITLE: - res = await self._device._send_key(ZKEYS.ZKEY_SUBTITLE) + res = await self._device.send_key(ZKEYS.ZKEY_SUBTITLE) elif cmd_id == Commands.DIGIT_0: - res = await self._device._send_key(ZKEYS.ZKEY_NUM_0) + res = await self._device.send_key(ZKEYS.ZKEY_NUM_0) elif cmd_id == Commands.DIGIT_1: - res = await self._device._send_key(ZKEYS.ZKEY_NUM_1) + res = await self._device.send_key(ZKEYS.ZKEY_NUM_1) elif cmd_id == Commands.DIGIT_2: - res = await self._device._send_key(ZKEYS.ZKEY_NUM_2) + res = await self._device.send_key(ZKEYS.ZKEY_NUM_2) elif cmd_id == Commands.DIGIT_3: - res = await self._device._send_key(ZKEYS.ZKEY_NUM_3) + res = await self._device.send_key(ZKEYS.ZKEY_NUM_3) elif cmd_id == Commands.DIGIT_4: - res = await self._device._send_key(ZKEYS.ZKEY_NUM_4) + res = await self._device.send_key(ZKEYS.ZKEY_NUM_4) elif cmd_id == Commands.DIGIT_5: - res = await self._device._send_key(ZKEYS.ZKEY_NUM_5) + res = await self._device.send_key(ZKEYS.ZKEY_NUM_5) elif cmd_id == Commands.DIGIT_6: - res = await self._device._send_key(ZKEYS.ZKEY_NUM_6) + res = await self._device.send_key(ZKEYS.ZKEY_NUM_6) elif cmd_id == Commands.DIGIT_7: - res = await self._device._send_key(ZKEYS.ZKEY_NUM_7) + res = await self._device.send_key(ZKEYS.ZKEY_NUM_7) elif cmd_id == Commands.DIGIT_8: - res = await self._device._send_key(ZKEYS.ZKEY_NUM_8) + res = await self._device.send_key(ZKEYS.ZKEY_NUM_8) elif cmd_id == Commands.DIGIT_9: - res = await self._device._send_key(ZKEYS.ZKEY_NUM_9) + res = await self._device.send_key(ZKEYS.ZKEY_NUM_9) else: return StatusCodes.NOT_IMPLEMENTED diff --git a/intg-zidoo/setup_flow.py b/intg-zidoo/setup_flow.py index 5e0045c..36a1e9f 100644 --- a/intg-zidoo/setup_flow.py +++ b/intg-zidoo/setup_flow.py @@ -11,7 +11,6 @@ import config import discover -from zidooaio import ZidooRC from config import DeviceInstance from ucapi import ( AbortDriverSetup, @@ -24,9 +23,12 @@ SetupError, UserDataResponse, ) +from zidooaio import ZidooRC _LOG = logging.getLogger(__name__) +# pylint: disable = W1405 + class SetupSteps(IntEnum): """Enumeration of setup steps to keep track of user data responses.""" @@ -82,7 +84,10 @@ async def driver_setup_handler(msg: SetupDriver) -> SetupAction: return await handle_driver_setup(msg) if isinstance(msg, UserDataResponse): _LOG.debug(msg) - if _setup_step == SetupSteps.CONFIGURATION_MODE and "action" in msg.input_values: + if ( + _setup_step == SetupSteps.CONFIGURATION_MODE + and "action" in msg.input_values + ): return await handle_configuration_mode(msg) if _setup_step == SetupSteps.DISCOVER and "address" in msg.input_values: return await _handle_discovery(msg) @@ -100,7 +105,9 @@ async def driver_setup_handler(msg: SetupDriver) -> SetupAction: return SetupError() -async def handle_driver_setup(_msg: DriverSetupRequest) -> RequestUserInput | SetupError: +async def handle_driver_setup( + _msg: DriverSetupRequest, +) -> RequestUserInput | SetupError: """ Start driver setup. @@ -120,7 +127,9 @@ async def handle_driver_setup(_msg: DriverSetupRequest) -> RequestUserInput | Se # get all configured devices for the user to choose from dropdown_devices = [] for device in config.devices.all(): - dropdown_devices.append({"id": device.id, "label": {"en": f"{device.name} ({device.id})"}}) + dropdown_devices.append( + {"id": device.id, "label": {"en": f"{device.name} ({device.id})"}} + ) # TODO #12 externalize language texts # build user actions, based on available devices @@ -165,7 +174,12 @@ async def handle_driver_setup(_msg: DriverSetupRequest) -> RequestUserInput | Se {"en": "Configuration mode", "de": "Konfigurations-Modus"}, [ { - "field": {"dropdown": {"value": dropdown_devices[0]["id"], "items": dropdown_devices}}, + "field": { + "dropdown": { + "value": dropdown_devices[0]["id"], + "items": dropdown_devices, + } + }, "id": "choice", "label": { "en": "Configured devices", @@ -174,7 +188,12 @@ async def handle_driver_setup(_msg: DriverSetupRequest) -> RequestUserInput | Se }, }, { - "field": {"dropdown": {"value": dropdown_actions[0]["id"], "items": dropdown_actions}}, + "field": { + "dropdown": { + "value": dropdown_actions[0]["id"], + "items": dropdown_actions, + } + }, "id": "action", "label": { "en": "Action", @@ -191,7 +210,9 @@ async def handle_driver_setup(_msg: DriverSetupRequest) -> RequestUserInput | Se return _user_input_discovery -async def handle_configuration_mode(msg: UserDataResponse) -> RequestUserInput | SetupComplete | SetupError: +async def handle_configuration_mode( + msg: UserDataResponse, +) -> RequestUserInput | SetupComplete | SetupError: """ Process user data response in a setup process. @@ -246,6 +267,7 @@ async def _handle_discovery(msg: UserDataResponse) -> RequestUserInput | SetupEr dropdown_items = [] address = msg.input_values["address"] + # pylint: disable = W0718 if address: _LOG.debug("Starting manual driver setup for %s", address) try: @@ -254,7 +276,9 @@ async def _handle_discovery(msg: UserDataResponse) -> RequestUserInput | SetupEr data = await device.connect() await device.disconnect() friendly_name = data["model"] - dropdown_items.append({"id": address, "label": {"en": f"{friendly_name} [{address}]"}}) + dropdown_items.append( + {"id": address, "label": {"en": f"{friendly_name} [{address}]"}} + ) except Exception as ex: _LOG.error("Cannot connect to manually entered address %s: %s", address, ex) return SetupError(error_type=IntegrationSetupError.CONNECTION_REFUSED) @@ -281,7 +305,12 @@ async def _handle_discovery(msg: UserDataResponse) -> RequestUserInput | SetupEr }, [ { - "field": {"dropdown": {"value": dropdown_items[0]["id"], "items": dropdown_items}}, + "field": { + "dropdown": { + "value": dropdown_items[0]["id"], + "items": dropdown_items, + } + }, "id": "choice", "label": { "en": "Choose your Zidoo", @@ -302,8 +331,11 @@ async def handle_device_choice(msg: UserDataResponse) -> SetupComplete | SetupEr :param msg: response data from the requested user data :return: the setup action on how to continue: SetupComplete if a valid AVR device was chosen. """ + # pylint: disable = W0718 host = msg.input_values["choice"] - _LOG.debug("Chosen Zidoo: %s. Trying to connect and retrieve device information...", host) + _LOG.debug( + "Chosen Zidoo: %s. Trying to connect and retrieve device information...", host + ) try: # connection check and mac_address extraction for wakeonlan device = ZidooRC(host, None) @@ -323,12 +355,20 @@ async def handle_device_choice(msg: UserDataResponse) -> SetupComplete | SetupEr unique_id = identifier if unique_id is None: - _LOG.error("Could not get mac address of host %s: required to create a unique device", host) + _LOG.error( + "Could not get mac address of host %s: required to create a unique device", + host, + ) return SetupError(error_type=IntegrationSetupError.OTHER) config.devices.add( - DeviceInstance(id=unique_id, name=friendly_name, address=host, - net_mac_address=net_mac_address, wifi_mac_address=wifi_mac_address) + DeviceInstance( + id=unique_id, + name=friendly_name, + address=host, + net_mac_address=net_mac_address, + wifi_mac_address=wifi_mac_address, + ) ) # triggers ZidooAVR instance creation config.devices.store() diff --git a/intg-zidoo/test.py b/intg-zidoo/test.py deleted file mode 100644 index df23d4a..0000000 --- a/intg-zidoo/test.py +++ /dev/null @@ -1,48 +0,0 @@ -import asyncio -import json -import socket - -import netifaces -from ssdp import aio, messages, network -import socket - -from zidooaio import ZidooRC - -class MyProtocol(aio.SimpleServiceDiscoveryProtocol): - - def response_received(self, response, addr): - print(response, addr) - - def request_received(self, request, addr): - print(request, addr) - - -# loop = asyncio.get_event_loop() -# connect = loop.create_datagram_endpoint(MyProtocol, family=socket.AF_INET) -# transport, protocol = loop.run_until_complete(connect) -# -# notify = messages.SSDPRequest('NOTIFY') -# notify.sendto(transport, (network.MULTICAST_ADDRESS_IPV4, network.PORT)) -# -# try: -# loop.run_forever() -# except KeyboardInterrupt: -# pass -# -# transport.close() -# loop.close() - -# toto = [i[4][0] for i in socket.getaddrinfo(socket.gethostname(), None)] -# print(*toto, sep=",") -# -# ips = [] -# for interface in netifaces.interfaces(): -# addresses = netifaces.ifaddresses(interface) -# for address in addresses.get(netifaces.AF_INET, []): -# ips.append(address["addr"]) -# print(*toto, sep=",") -loop=asyncio.new_event_loop() -device = ZidooRC("192.168.1.40", loop=loop) -data = asyncio.run(device.connect()) -asyncio.run(device.disconnect()) -print(json.dumps(data)) diff --git a/intg-zidoo/zidooaio.py b/intg-zidoo/zidooaio.py index 7929d2d..2234af3 100644 --- a/intg-zidoo/zidooaio.py +++ b/intg-zidoo/zidooaio.py @@ -1,5 +1,6 @@ """ -Zidoo Remote Control API +Zidoo Remote Control API. + By Wizmo References oem v1: https://www.zidoo.tv/Support/developer/ @@ -7,25 +8,27 @@ """ import asyncio -import logging import json +import logging import socket import struct +import urllib.parse from asyncio import AbstractEventLoop, Lock -from enum import IntEnum, StrEnum -from ucapi.media_player import Attributes as MediaAttr -from aiohttp import ClientError, ClientSession, CookieJar from datetime import datetime, timedelta -import urllib.parse +from enum import IntEnum, StrEnum +from aiohttp import ClientError, ClientSession, CookieJar +from config import DeviceInstance from pyee import AsyncIOEventEmitter +from ucapi.media_player import Attributes as MediaAttr from ucapi.media_player import MediaType from yarl import URL -from config import DeviceInstance SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL_RAPID = timedelta(seconds=1) +# pylint: disable = C0302 + class Events(IntEnum): """Internal driver events.""" @@ -59,10 +62,10 @@ class States(IntEnum): DEFAULT_COUNT = 250 # default list limit ZCMD_STATUS = "getPlayStatus" -"""Remote Control Button keys""" - class ZKEYS(StrEnum): + """List of available keys.""" + ZKEY_BACK = "Key.Back" ZKEY_CANCEL = "Key.Cancel" ZKEY_HOME = "Key.Home" @@ -124,7 +127,7 @@ class ZKEYS(StrEnum): ZKEY_APP_SWITCH = "Key.APP.Switch" -"""Movie Player entry types.""" +# Movie Player entry types ZTYPE_VIDEO = 0 ZTYPE_MOVIE = 1 ZTYPE_COLLECTION = 2 @@ -224,7 +227,7 @@ class ZKEYS(StrEnum): } -class ZidooRC(object): +class ZidooRC: """Zidoo Media Player Remote Control.""" def __init__( @@ -246,6 +249,7 @@ def __init__( authorization password key. If not assigned, standard basic auth is used. """ self._device_config = device_config + self._mac = mac self._wifi_mac = None self._ethernet_mac = None if device_config: @@ -258,7 +262,7 @@ def __init__( self.events = AsyncIOEventEmitter(self.event_loop) self._source_list = None self._media_type: MediaType | None = None - self._host = "{}:{}".format(host, CONF_PORT) + self._host = f"{host}:{CONF_PORT}" self._psk = psk self._session = None self._cookies = None @@ -283,10 +287,12 @@ def __init__( @property def state(self) -> States: + """Return device state.""" return self._state @property def media_type(self): + """Return current media type.""" return self._media_type @property @@ -326,6 +332,7 @@ def media_duration(self): duration = self._media_info.get("duration") if duration: return float(duration) / 1000 + return None @property def media_position(self): @@ -333,6 +340,7 @@ def media_position(self): position = self._media_info.get("position") if position: return float(position) / 1000 + return None @property def media_image_url(self): @@ -395,6 +403,7 @@ def _filter_updated_media_info(self, media_info: any, updated_data: any) -> any: async def async_update_data(self) -> None: """Update data callback.""" + # pylint: disable = R0915,R1702 if not self.is_connected(): await self.connect() updated_data = {} @@ -455,13 +464,15 @@ async def async_update_data(self) -> None: self._media_type = media_type updated_data[MediaAttr.MEDIA_TYPE] = self._media_type - except Exception as err: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except return if state != self._last_state: _LOGGER.debug("%s New state (%s)", self._device_config.name, state) self._last_state = state - self._update_interval = SCAN_INTERVAL if state == States.OFF else SCAN_INTERVAL_RAPID + self._update_interval = ( + SCAN_INTERVAL if state == States.OFF else SCAN_INTERVAL_RAPID + ) updated_data[MediaAttr.STATE] = state self._state = state if updated_data: @@ -469,9 +480,13 @@ async def async_update_data(self) -> None: def _jdata_build(self, method: str, params: dict = None) -> str: if params: - ret = json.dumps({"method": method, "params": [params], "id": 1, "version": "1.0"}) + ret = json.dumps( + {"method": method, "params": [params], "id": 1, "version": "1.0"} + ) else: - ret = json.dumps({"method": method, "params": [], "id": 1, "version": "1.0"}) + ret = json.dumps( + {"method": method, "params": [], "id": 1, "version": "1.0"} + ) return ret async def _init_network(self): @@ -485,11 +500,11 @@ async def _init_network(self): for item in data["list"]: url = urllib.parse.quote(item.get("url"), safe="") await self._req_json( - "ZidooFileControl/v2/getFiles?requestCount=100&startIndex=0&sort=0&url={}".format(url) + f"ZidooFileControl/v2/getFiles?requestCount=100&startIndex=0&sort=0&url={url}" ) # gets current song list (and appears to initialize network shared on old devices) await self.get_music_playlist() - print("SONG_LIST: {}".format(self._song_list)) + # print(f"SONG_LIST: {self._song_list}") # _LOGGER.debug(response) # await self._req_json("ZidooFileControl/v2/getUpnpDevices") @@ -513,6 +528,7 @@ async def connect(self) -> json: # url = "ZidooControlCenter/connect?name={}&uuid={}&tag=0".format(client_name, client_uuid) # response = await self._req_json(url, log_errors=False) + # pylint: disable = W0718 try: response = await self.get_system_info(log_errors=False) @@ -569,7 +585,7 @@ def _wakeonlan(self) -> None: socket_instance.sendto(msg, ("<broadcast>", 9)) socket_instance.close() - async def _send_key(self, key: str, log_errors: bool = False) -> bool: + async def send_key(self, key: str, log_errors: bool = False) -> bool: """Async Send Remote Control button command to device. Parameters @@ -592,6 +608,7 @@ async def _send_key(self, key: str, log_errors: bool = False) -> bool: async def _req_json( self, url: str, + # pylint: disable = W0102 params: dict = {}, log_errors: bool = True, timeout: int = TIMEOUT, @@ -614,7 +631,6 @@ async def _req_json( json raw API response """ - while max_retries >= 0: response = await self._send_cmd(url, params, log_errors, timeout) @@ -658,7 +674,9 @@ async def _send_cmd( raw API response """ if self._session is None: - self._session = ClientSession(cookie_jar=CookieJar(unsafe=True, quote_cookie=False)) + self._session = ClientSession( + cookie_jar=CookieJar(unsafe=True, quote_cookie=False) + ) headers = {} if self._psk is not None: @@ -667,7 +685,7 @@ async def _send_cmd( headers["Cache-Control"] = "no-cache" headers["Connection"] = "keep-alive" - url = "http://{}/{}".format(self._host, url) + url = f"http://{self._host}/{url}" try: response = await self._session.get( @@ -752,7 +770,9 @@ async def get_playing_info(self) -> json: async def _get_video_playing_info(self) -> json: """Async Get information from built in video player.""" return_value = {} - response = await self._req_json("ZidooVideoPlay/" + ZCMD_STATUS, log_errors=False, timeout=TIMEOUT_INFO) + response = await self._req_json( + "ZidooVideoPlay/" + ZCMD_STATUS, log_errors=False, timeout=TIMEOUT_INFO + ) if response is not None and response.get("status") == 200: if response.get("subtitle"): @@ -789,7 +809,9 @@ async def _get_id_from_uri(self, uri: str) -> int: movie_id = 0 movie_info = {} - response = await self._req_json("ZidooPoster/v2/getAggregationOfFile?path={}".format(urllib.parse.quote(uri))) + response = await self._req_json( + f"ZidooPoster/v2/getAggregationOfFile?path={urllib.parse.quote(uri)}" + ) if response: movie_info["type"] = response.get("type") @@ -824,8 +846,11 @@ async def _get_id_from_uri(self, uri: str) -> int: async def _get_music_playing_info(self) -> json: """Async Get information from built in Music Player.""" return_value = {} - response = await self._req_json("ZidooMusicControl/" + ZCMD_STATUS, log_errors=False, timeout=TIMEOUT_INFO) + response = await self._req_json( + "ZidooMusicControl/" + ZCMD_STATUS, log_errors=False, timeout=TIMEOUT_INFO + ) + # pylint: disable = W1405 if response is not None and response.get("status") == 200: return_value["status"] = response.get("isPlay") result = response.get("music") @@ -836,12 +861,8 @@ async def _get_music_playing_info(self) -> json: return_value["date"] = result.get("date") return_value["uri"] = result.get("uri") return_value["bitrate"] = result.get("bitrate") - return_value["audio"] = "{}: {} channels {} bits {} Hz".format( - result.get("extension"), - result.get("channels"), - result.get("bits"), - result.get("SampleRate"), - ) + return_value["audio"] = (f"{result.get('extension')}: {result.get('channels')}" + f" channels {result.get('bits')} bits {result.get('SampleRate')} Hz") self._music_id = result.get("id") self._music_type = result.get("type") @@ -861,7 +882,9 @@ async def _get_movie_playing_info(self) -> json: """Async Get information from built in Movie Player.""" return_value = {} - response = await self._req_json("ZidooControlCenter/" + ZCMD_STATUS, log_errors=False, timeout=TIMEOUT_INFO) + response = await self._req_json( + "ZidooControlCenter/" + ZCMD_STATUS, log_errors=False, timeout=TIMEOUT_INFO + ) if response is not None and response.get("status") == 200: if response.get("file"): @@ -902,7 +925,9 @@ async def get_subtitle_list(self, log_errors=True) -> dict: dictionary list """ return_values = {} - response = await self._req_json("ZidooVideoPlay/getSubtitleList", log_errors=log_errors) + response = await self._req_json( + "ZidooVideoPlay/getSubtitleList", log_errors=log_errors + ) if response is not None and response.get("status") == 200: for result in response["subtitles"]: @@ -921,9 +946,13 @@ async def set_subtitle(self, index: int = None) -> bool: True if successful """ if index is None: - index = self._next_data(await self.get_subtitle_list(), self._current_subtitle) + index = self._next_data( + await self.get_subtitle_list(), self._current_subtitle + ) - response = await self._req_json("ZidooVideoPlay/setSubtitle?index={}".format(index), log_errors=False) + response = await self._req_json( + f"ZidooVideoPlay/setSubtitle?index={index}", log_errors=False + ) if response is not None and response.get("status") == 200: self._current_subtitle = index @@ -959,7 +988,9 @@ async def set_audio(self, index: int = None) -> bool: if index is None: index = self._next_data(await self.get_audio_list(), self._current_audio) - response = await self._req_json("ZidooVideoPlay/setAudio?index={}".format(index), log_errors=False) + response = await self._req_json( + f"ZidooVideoPlay/setAudio?index={index}", log_errors=False + ) if response is not None and response.get("status") == 200: self._current_audio = index @@ -987,7 +1018,9 @@ async def get_system_info(self, log_errors=True) -> json: 'ableRemoteBoot': network boot compatible (wol) 'pyapiversion': python api version """ - response = await self._req_json("ZidooControlCenter/getModel", log_errors=log_errors, max_retries=0) + response = await self._req_json( + "ZidooControlCenter/getModel", log_errors=log_errors, max_retries=0 + ) if response and response.get("status") == 200: response["pyapiversion"] = VERSION @@ -1007,7 +1040,7 @@ async def get_power_status(self) -> str: if response and response.get("status") == 200: self._power_status = True - except: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except pass if self._power_status is True: @@ -1016,10 +1049,12 @@ async def get_power_status(self) -> str: async def get_volume_info(self): """Async Get volume info. Not currently supported.""" + # pylint: disable = W0613 return None async def set_volume_level(self, volume): """Async Set volume level. Not currently supported.""" + # pylint: disable = W0613 # api_volume = str(int(round(volume * 100))) return 0 @@ -1032,7 +1067,9 @@ async def get_app_list(self, log_errors=True) -> dict: """ return_values = {} - response = await self._req_json("ZidooControlCenter/Apps/getApps", log_errors=log_errors) + response = await self._req_json( + "ZidooControlCenter/Apps/getApps", log_errors=log_errors + ) if response is not None and response.get("status") == 200: for result in response["apps"]: @@ -1054,12 +1091,16 @@ async def start_app(self, app_name: str, log_errors=True) -> bool: if len(self._app_list) == 0: self._app_list = await self.get_app_list(log_errors) if app_name in self._app_list: - return await self._start_app(self._app_list[app_name], log_errors=log_errors) + return await self._start_app( + self._app_list[app_name] + ) return False - async def _start_app(self, app_id, log_errors=True) -> bool: + async def _start_app(self, app_id) -> bool: """Async Start an app by package name.""" - response = await self._req_json("ZidooControlCenter/Apps/openApp?packageName={}".format(app_id)) + response = await self._req_json( + f"ZidooControlCenter/Apps/openApp?packageName={app_id}" + ) if response is not None and response.get("status") == 200: return True @@ -1092,20 +1133,20 @@ async def get_movie_list(self, filter_type=-1, max_count=DEFAULT_COUNT) -> json: raw API response if successful """ - def byId(e): + def by_id(e): return e["id"] if filter_type in ZVIDEO_FILTER_TYPES: - filter_type = ZVIDEO_FILTER_TYPES[filter_type] + filter_type = ZVIDEO_FILTER_TYPES.get(filter_type, None) # v2 http://{{host}}/Poster/v2/getAggregations?type=0&start=0&count=40 response = await self._req_json( - "ZidooPoster/getVideoList?page=1&pagesize={}&type={}".format(max_count, filter_type) + f"ZidooPoster/getVideoList?page=1&pagesize={max_count}&type={filter_type}" ) if response is not None and response.get("status") == 200: if filter_type in {10, 11}: - response["data"].sort(key=byId, reverse=True) + response["data"].sort(key=by_id, reverse=True) return response async def get_collection_list(self, movie_id) -> json: @@ -1118,7 +1159,9 @@ async def get_collection_list(self, movie_id) -> json: json raw API response if successful """ - response = await self._req_json("ZidooPoster/getCollection?id={}".format(movie_id)) + response = await self._req_json( + f"ZidooPoster/getCollection?id={movie_id}" + ) if response is not None and response.get("status") == 200: return response @@ -1134,7 +1177,7 @@ async def get_movie_details(self, movie_id: int) -> json: raw API response (no status) """ # v1 response = self._req_json("ZidooPoster/getDetail?id={}".format(movie_id)) - response = await self._req_json("Poster/v2/getDetail?id={}".format(movie_id)) + response = await self._req_json(f"Poster/v2/getDetail?id={movie_id}") if response is not None: # and response.get("status") == 200: return response @@ -1150,7 +1193,7 @@ async def get_episode_list(self, season_id: int) -> json: raw API episode list if successful """ - def byEpisode(e): + def by_episode(e): return e["aggregation"]["episodeNumber"] response = await self.get_movie_details(season_id) @@ -1164,7 +1207,7 @@ def byEpisode(e): episodes = result.get("aggregations") if episodes is not None: - episodes.sort(key=byEpisode) + episodes.sort(key=by_episode) return episodes async def _collection_video_id(self, movie_id: int) -> int: @@ -1177,7 +1220,9 @@ async def _collection_video_id(self, movie_id: int) -> int: return result["aggregationId"] return movie_id - async def get_music_list(self, music_type: int = 0, music_id: int = None, max_count: int = DEFAULT_COUNT) -> json: + async def get_music_list( + self, music_type: int = 0, music_id: int = None, max_count: int = DEFAULT_COUNT + ) -> json: """Async Return list of music. Parameters @@ -1191,9 +1236,9 @@ async def get_music_list(self, music_type: int = 0, music_id: int = None, max_co """ if music_type == ZMEDIA_TYPE_ARTIST: return await self._get_artist_list(music_id, max_count) - elif music_type == ZMEDIA_TYPE_ALBUM: + if music_type == ZMEDIA_TYPE_ALBUM: return await self._get_album_list(music_id, max_count) - elif music_type == ZMEDIA_TYPE_PLAYLIST: + if music_type == ZMEDIA_TYPE_PLAYLIST: return await self._get_playlist_list(music_id, max_count) return await self._get_song_list(max_count) @@ -1207,13 +1252,17 @@ async def _get_song_list(self, max_count: int = DEFAULT_COUNT) -> json: json raw API response if successful """ - response = await self._req_json("MusicControl/v2/getSingleMusics?start=0&count={}".format(max_count)) + response = await self._req_json( + f"MusicControl/v2/getSingleMusics?start=0&count={max_count}" + ) self._song_list = self._get_music_ids(response.get("array")) if response is not None: return response - async def _get_album_list(self, album_id: int = None, max_count: int = DEFAULT_COUNT) -> json: + async def _get_album_list( + self, album_id: int = None, max_count: int = DEFAULT_COUNT + ) -> json: """Async Return list of albums or album music. Parameters @@ -1227,15 +1276,19 @@ async def _get_album_list(self, album_id: int = None, max_count: int = DEFAULT_C """ if album_id: response = await self._req_json( - "MusicControl/v2/getAlbumMusics?id={}&start=0&count={}".format(album_id, max_count) + f"MusicControl/v2/getAlbumMusics?id={album_id}&start=0&count={max_count}" ) else: - response = await self._req_json("MusicControl/v2/getAlbums?start=0&count={}".format(max_count)) + response = await self._req_json( + f"MusicControl/v2/getAlbums?start=0&count={max_count}" + ) if response is not None: return response - async def _get_artist_list(self, artist_id: int = None, max_count: int = DEFAULT_COUNT) -> json: + async def _get_artist_list( + self, artist_id: int = None, max_count: int = DEFAULT_COUNT + ) -> json: """Async Return list of artists or artist music. Parameters @@ -1251,10 +1304,12 @@ async def _get_artist_list(self, artist_id: int = None, max_count: int = DEFAULT """ if artist_id: response = await self._req_json( - "MusicControl/v2/getArtistMusics?id={}&start=0&count={}".format(artist_id, max_count) + f"MusicControl/v2/getArtistMusics?id={artist_id}&start=0&count={max_count}" ) else: - response = await self._req_json("MusicControl/v2/getArtists?start=0&count={}".format(max_count)) + response = await self._req_json( + f"MusicControl/v2/getArtists?start=0&count={max_count}" + ) if response is not None: # and response.get("status") == 200: return response @@ -1273,12 +1328,14 @@ async def _get_playlist_list(self, playlist_id=None, max_count=DEFAULT_COUNT): """ if playlist_id: if playlist_id == "playing": - response = await self._req_json("MusicControl/v2/getPlayQueue?start=0&count={}".format(max_count)) + response = await self._req_json( + f"MusicControl/v2/getPlayQueue?start=0&count={max_count}" + ) if response: self._song_list = self._get_music_ids(response.get("array")) else: response = await self._req_json( - "MusicControl/v2/getSongListMusics?id={}&start=0&count={}".format(playlist_id, max_count) + f"MusicControl/v2/getSongListMusics?id={playlist_id}&start=0&count={max_count}" ) else: response = await self._req_json( @@ -1289,7 +1346,9 @@ async def _get_playlist_list(self, playlist_id=None, max_count=DEFAULT_COUNT): if response is not None: return response - async def search_movies(self, query: str, search_type: int = 0, max_count: int = DEFAULT_COUNT) -> json: + async def search_movies( + self, search_type: int | str = 0, max_count: int = DEFAULT_COUNT + ) -> json: """Async Return list of video based on query. Parameters @@ -1302,11 +1361,11 @@ async def search_movies(self, query: str, search_type: int = 0, max_count: int = raw API response (no status) """ if search_type in ZVIDEO_SEARCH_TYPES: - search_type = ZVIDEO_SEARCH_TYPES[search_type] + search_type = ZVIDEO_SEARCH_TYPES.get(search_type, 0) # v1 "ZidooPoster/search?q={}&type={}&page=1&pagesize={}".format(query, filter_type, max_count) response = await self._req_json( - "Poster/v2/searchAggregation?q={}&type={}&start=0&count={}".format(query, search_type, max_count), + f"Poster/v2/searchAggregation?q={max_count}&type={search_type}&start=0&count={max_count}", timeout=TIMEOUT_SEARCH, ) @@ -1316,7 +1375,7 @@ async def search_movies(self, query: str, search_type: int = 0, max_count: int = async def search_music( self, query: str, - search_type: int = 0, + search_type: int | str = 0, max_count: int = DEFAULT_COUNT, play: bool = False, ) -> json: @@ -1336,11 +1395,11 @@ async def search_music( raw API response (no status) """ if search_type in ZMUSIC_SEARCH_TYPES: - search_type = ZMUSIC_SEARCH_TYPES[search_type] + search_type = ZMUSIC_SEARCH_TYPES.get(search_type, 0) if search_type == 1: return await self._search_album(query, max_count) - elif search_type == 2: + if search_type == 2: return await self._search_artist(query, max_count) response = await self._search_song(query, max_count) if response: @@ -1360,7 +1419,7 @@ async def _search_song(self, query: str, max_count: int = DEFAULT_COUNT) -> json raw API response (no status) """ response = await self._req_json( - "MusicControl/v2/searchMusic?key={}&start=0&count={}".format(query, max_count), + f"MusicControl/v2/searchMusic?key={query}&start=0&count={max_count}", timeout=TIMEOUT_SEARCH, ) @@ -1378,7 +1437,7 @@ async def _search_album(self, query: str, max_count: int = DEFAULT_COUNT) -> jso raw API response (no status) """ response = await self._req_json( - "MusicControl/v2/searchAlbum?key={}&start=0&count={}".format(query, max_count), + f"MusicControl/v2/searchAlbum?key={query}&start=0&count={max_count}", timeout=TIMEOUT_SEARCH, ) @@ -1396,7 +1455,7 @@ async def _search_artist(self, query: str, max_count: int = DEFAULT_COUNT) -> js raw API response (no status) """ response = await self._req_json( - "MusicControl/v2/searchArtist?key={}&start=0&count={}".format(query, max_count), + f"MusicControl/v2/searchArtist?key={query}&start=0&count={max_count}", timeout=TIMEOUT_SEARCH, ) @@ -1412,9 +1471,8 @@ async def play_file(self, uri: str) -> bool: Returns True if successful """ - url = "ZidooFileControl/openFile?path={}&videoplaymode={}".format( - uri, 0 - ) # has issues with parsing for local files + url = f"ZidooFileControl/openFile?path={uri}&videoplaymode={0}" + # has issues with parsing for local files response = await self._req_json(url) @@ -1451,19 +1509,20 @@ async def play_stream(self, uri: str, media_type) -> bool: Returns True if successful """ + # pylint: disable = W1405 # take major form mime type if isinstance(media_type, str) and "/" in media_type: media_type = media_type.split("/")[0] if media_type in ZTYPE_MIMETYPE: - media_type = ZTYPE_MIMETYPE[media_type] + media_type = ZTYPE_MIMETYPE.get(media_type) # the res uri needs to be double quoted to protect keys etc. # use safe='' in quote to force "/" quoting uri = urllib.parse.quote(uri, safe="") - upnp = "upnp://{}/{}?type={}&res={}".format(ZUPNP_SERVERNAME, VERSION, media_type, uri) - url = "ZidooFileControl/v2/openFile?url={}".format(urllib.parse.quote(upnp, safe="")) + upnp = f"upnp://{ZUPNP_SERVERNAME}/{VERSION}?type={media_type}&res={uri}" + url = f"ZidooFileControl/v2/openFile?url={urllib.parse.quote(upnp, safe='')}" _LOGGER.debug("Stream command %s", str(url)) response = await self._req_json(url) @@ -1487,7 +1546,9 @@ async def play_movie(self, movie_id: int, video_type: int = -1) -> bool: # print("Video id : {}".format(video_id)) # v2 http://{}/VideoPlay/playVideo?index=0 - response = await self._req_json("ZidooPoster/PlayVideo?id={}&type={}".format(movie_id, video_type)) + response = await self._req_json( + f"ZidooPoster/PlayVideo?id={movie_id}&type={video_type}" + ) if response and response.get("status") == 200: return True @@ -1499,11 +1560,13 @@ def _get_music_ids(self, data, key="id", sub=None): for item in data: result = item.get(sub) if sub else item if result: - id = result.get(key) - ids.append(str(id)) + _id = result.get(key) + ids.append(str(_id)) return ids - async def play_music(self, media_id: int = None, media_type: int = "music", music_id: int = None) -> bool: + async def play_music( + self, media_id: int = None, media_type: int = "music", music_id: int = None + ) -> bool: """Async Play video content by id. Parameters @@ -1516,17 +1579,15 @@ async def play_music(self, media_id: int = None, media_type: int = "music", musi Returns True if successfull """ + # pylint: disable = W1405 if media_type in ZMUSIC_PLAYLISTTYPE and media_id != "playing": response = await self._req_json( - "MusicControl/v2/playMusic?type={}&id={}&musicId={}&music_type=0&trackIndex=1&sort=0".format( - ZMUSIC_PLAYLISTTYPE[media_type], media_id, music_id - ) + f"MusicControl/v2/playMusic?type={ZMUSIC_PLAYLISTTYPE[media_type]}" + f"&id={media_id}&musicId={music_id}&music_type=0&trackIndex=1&sort=0" ) else: # music response = await self._req_json( - "MusicControl/v2/playMusics?ids={}&musicId={}&trackIndex=-1".format( - "%2C".join(self._song_list), music_id - ) + f"MusicControl/v2/playMusics?ids={'%2C'.join(self._song_list)}&musicId={music_id}&trackIndex=-1" ) if response and response.get("status") == 200: @@ -1558,7 +1619,9 @@ async def get_music_playlist(self, max_count: int = DEFAULT_COUNT) -> json: Returns raw api response if successful """ - response = await self._req_json("MusicControl/v2/getPlayQueue?start=0&count={}".format(max_count)) + response = await self._req_json( + f"MusicControl/v2/getPlayQueue?start=0&count={max_count}" + ) if response is not None: return response @@ -1580,7 +1643,9 @@ async def get_file_list(self, uri: str, file_type: int = 0) -> json: 'length': length in ms 'modifyDate': linux date code """ - response = await self._req_json("ZidooFileControl/getFileList?path={}&type={}".format(uri, file_type)) + response = await self._req_json( + f"ZidooFileControl/getFileList?path={uri}&type={file_type}" + ) if response is not None and response.get("status") == 200: return response @@ -1600,7 +1665,9 @@ async def get_host_list(self, uri: str, host_type: int = 1005) -> json: 'length': length in ms 'modifyDate': linux date code """ - response = await self._req_json("ZidooFileControl/getHost?path={}&type={}".format(uri, host_type)) + response = await self._req_json( + f"ZidooFileControl/getHost?path={uri}&type={host_type}" + ) _LOGGER.debug("zidoo host list: %s", str(response)) return_value = {} @@ -1618,7 +1685,9 @@ async def get_host_list(self, uri: str, host_type: int = 1005) -> json: return_value["filelist"] = share_list return return_value - def generate_image_url(self, media_id: int, media_type: int, width: int = 400, height: int = None) -> str: + def generate_image_url( + self, media_id: int, media_type: int, width: int = 400, height: int = None + ) -> str: """Get link to thumbnail.""" if media_type in ZVIDEO_SEARCH_TYPES: if height is None: @@ -1627,9 +1696,14 @@ def generate_image_url(self, media_id: int, media_type: int, width: int = 400, h if media_type in ZMUSIC_SEARCH_TYPES: if height is None: height = width - return self._generate_music_image_url(media_id, ZMUSIC_SEARCH_TYPES[media_type], width, height) + return self._generate_music_image_url( + media_id, ZMUSIC_SEARCH_TYPES[media_type], width, height + ) + return None - def _generate_movie_image_url(self, movie_id: int, width: int = 400, height: int = 600) -> str: + def _generate_movie_image_url( + self, movie_id: int, width: int = 400, height: int = 600 + ) -> str: """Get link to thumbnail. Parameters @@ -1643,11 +1717,14 @@ def _generate_movie_image_url(self, movie_id: int, width: int = 400, height: int url for image """ # http://{}/Poster/v2/getPoster?id=66&w=60&h=30 - url = "http://{}/ZidooPoster/getFile/getPoster?id={}&w={}&h={}".format(self._host, movie_id, width, height) + url = f"http://{self._host}/ZidooPoster/getFile/getPoster?id={movie_id}&w={width}&h={height}" return url - def _generate_music_image_url(self, music_id: int, music_type: int = 0, width: int = 200, height: int = 200) -> str: + # pylint: disable = W0613 + def _generate_music_image_url( + self, music_id: int, music_type: int = 0, width: int = 200, height: int = 200 + ) -> str: """Get link to thumbnail. Parameters @@ -1660,13 +1737,9 @@ def _generate_music_image_url(self, music_id: int, music_type: int = 0, width: i Returns url for image """ - url = "http://{}/ZidooMusicControl/v2/getImage?id={}&music_type={}&type={}&target={}".format( - self._host, - music_id, - ZMUSIC_IMAGETYPE[music_type], - music_type, - ZMUSIC_IMAGETARGET[music_type], - ) + url = (f"http://{self._host}/ZidooMusicControl/v2/getImage?id={music_id}" + f"&music_type={ZMUSIC_IMAGETYPE[music_type]}" + f"&type={music_type}&target={ZMUSIC_IMAGETARGET[music_type]}") return url @@ -1686,14 +1759,11 @@ def generate_current_image_url(self, width: int = 1080, height: int = 720) -> st url = None if self._current_source == ZCONTENT_VIDEO and self._video_id > 0: - url = "http://{}/ZidooPoster/getFile/getBackdrop?id={}&w={}&h={}".format( - self._host, self._video_id, width, height - ) + url = f"http://{self._host}/ZidooPoster/getFile/getBackdrop?id={self._video_id}&w={width}&h={height}" if self._current_source == ZCONTENT_MUSIC and self._music_id > 0: - url = "http://{}/ZidooMusicControl/v2/getImage?id={}&music_type={}&type=4&target=16".format( - self._host, self._music_id, self._music_type - ) + url = (f"http://{self._host}/ZidooMusicControl/v2/getImage?" + f"id={self._music_id}&music_type={self._music_type}&type=4&target=16") # _LOGGER.debug("zidoo getting current image: url-{}".format(url)) return url @@ -1708,19 +1778,21 @@ async def turn_on(self): async def turn_off(self, standby=False): """Async Turn off media player.""" - return await self._send_key(ZKEYS.ZKEY_POWER_STANDBY if standby else ZKEYS.ZKEY_POWER_OFF) + return await self.send_key( + ZKEYS.ZKEY_POWER_STANDBY if standby else ZKEYS.ZKEY_POWER_OFF + ) async def volume_up(self): """Async Volume up the media player.""" - return await self._send_key(ZKEYS.ZKEY_VOLUME_UP) + return await self.send_key(ZKEYS.ZKEY_VOLUME_UP) async def volume_down(self): """Async Volume down media player.""" - return await self._send_key(ZKEYS.ZKEY_VOLUME_DOWN) + return await self.send_key(ZKEYS.ZKEY_VOLUME_DOWN) async def mute_volume(self): """Async Send mute command.""" - return self._send_key(ZKEYS.ZKEY_MUTE) + return self.send_key(ZKEYS.ZKEY_MUTE) async def media_play_pause(self): """Async Send play or Pause command.""" @@ -1728,39 +1800,36 @@ async def media_play_pause(self): return await self.media_pause() return await self.media_play() - async def media_play(self): + async def media_play(self) -> any: """Async Send play command.""" # self._send_key(ZKEYS.ZKEY_OK) if self._current_source == ZCONTENT_NONE and self._last_video_path: return await self.play_file(self._last_video_path) - elif self._current_source == ZCONTENT_MUSIC: + if self._current_source == ZCONTENT_MUSIC: return await self._req_json("MusicControl/v2/playOrPause") - return await self._send_key(ZKEYS.ZKEY_MEDIA_PLAY) + return await self.send_key(ZKEYS.ZKEY_MEDIA_PLAY) async def media_pause(self): """Async Send media pause command to media player.""" if self._current_source == ZCONTENT_MUSIC: return await self._req_json("MusicControl/v2/playOrPause") - else: - return await self._send_key(ZKEYS.ZKEY_MEDIA_PAUSE) + return await self.send_key(ZKEYS.ZKEY_MEDIA_PAUSE) async def media_stop(self): """Async Send media pause command to media player.""" - return await self._send_key(ZKEYS.ZKEY_MEDIA_STOP) + return await self.send_key(ZKEYS.ZKEY_MEDIA_STOP) async def media_next_track(self): """Async Send next track command.""" if self._current_source == ZCONTENT_MUSIC: return await self._req_json("MusicControl/v2/playNext") - else: - return await self._send_key(ZKEYS.ZKEY_MEDIA_NEXT) + return await self.send_key(ZKEYS.ZKEY_MEDIA_NEXT) async def media_previous_track(self): """Async Send the previous track command.""" if self._current_source == ZCONTENT_MUSIC: return await self._req_json("MusicControl/v2/playLast") - else: - await self._send_key(ZKEYS.ZKEY_MEDIA_PREVIOUS) + await self.send_key(ZKEYS.ZKEY_MEDIA_PREVIOUS) async def set_media_position(self, position): """Async Set the current playing position. @@ -1783,14 +1852,18 @@ async def set_media_position(self, position): async def _set_movie_position(self, position): """Async Set current position for video player.""" - response = await self._req_json("ZidooVideoPlay/seekTo?positon={}".format(int(position))) + response = await self._req_json( + f"ZidooVideoPlay/seekTo?positon={int(position)}" + ) if response is not None and response.get("status") == 200: return response async def _set_audio_position(self, position): """Async Set current position for music player.""" - response = await self._req_json("ZidooMusicControl/seekTo?time={}".format(int(position))) + response = await self._req_json( + f"ZidooMusicControl/seekTo?time={int(position)}" + ) if response is not None and response.get("status") == 200: return response diff --git a/requirements.txt b/requirements.txt index 957ba97..7dc729e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,6 @@ pyee>=9.0 -ucapi==0.1.7 +ucapi==0.2.0 -logging~=0.4.9.6 -requests~=2.31.0 -ssdp~=1.3.0 -requests~=2.31.0 httpx~=0.27.0 defusedxml~=0.7.1 aiohttp~=3.9.3