[
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n  \"name\": \"SamsungTV Smart Integration\",\n  \"dockerFile\": \"../Dockerfile.dev\",\n  \"postCreateCommand\": \"scripts/setup\",\n  \"forwardPorts\": [8123],\n  \"portsAttributes\": {\n    \"8123\": {\n      \"label\": \"Home Assistant\",\n      \"onAutoForward\": \"notify\"\n    }\n  },\n  \"customizations\": {\n    \"vscode\": {\n      \"extensions\": [\n        \"ms-python.black-formatter\",\n        \"ms-python.pylint\",\n        \"ms-python.vscode-pylance\",\n        \"visualstudioexptteam.vscodeintellicode\",\n        \"redhat.vscode-yaml\",\n        \"esbenp.prettier-vscode\",\n        \"GitHub.vscode-pull-request-github\",\n        \"ryanluker.vscode-coverage-gutters\"\n      ],\n      \"settings\": {\n        \"files.eol\": \"\\n\",\n        \"editor.tabSize\": 4,\n        \"python.pythonPath\": \"/usr/local/bin/python\",\n        \"python.testing.pytestArgs\": [\"--no-cov\"],\n        \"python.analysis.autoSearchPaths\": false,\n        \"editor.formatOnPaste\": false,\n        \"editor.formatOnSave\": true,\n        \"editor.formatOnType\": true,\n        \"files.trimTrailingWhitespace\": true,\n        \"terminal.integrated.profiles.linux\": {\n          \"zsh\": {\n            \"path\": \"/usr/bin/zsh\"\n          }\n        },\n        \"terminal.integrated.defaultProfile.linux\": \"zsh\",\n        \"[python]\": {\n          \"editor.defaultFormatter\": \"ms-python.black-formatter\"\n        }\n      }\n    }\n  },\n  \"remoteUser\": \"vscode\"\n}"
  },
  {
    "path": ".dockerignore",
    "content": "# General files\n.git\n.github\nconfig\ndocs\n\n# Development\n.devcontainer\n.vscode\n\n# Test related files\ntests\n\n# Other virtualization methods\nvenv\n.vagrant\n\n# Temporary files\n**/__pycache__"
  },
  {
    "path": ".gitattributes",
    "content": "# Ensure Docker script files uses LF to support Docker for Windows.\n# Ensure \"git config --global core.autocrlf input\" before you clone\n*     text eol=lf\n*.py  whitespace=error\n\n*.ico binary\n*.gif binary\n*.jpg binary\n*.png binary\n*.zip binary\n*.mp3 binary\n\nDockerfile.dev linguist-language=Dockerfile\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**Expected behavior**\nIf applicable, a clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Environment details:**\n - Environment (HASSIO, Raspbian, etc):\n - Home Assistant version installed:\n - Component version installed:\n - Last know working version:\n - TV model:\n\n**Output of HA logs**\nPaste the relavant output of the HA log here.\n\n```\n\n```\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: Feature Request\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/hassfest.yaml",
    "content": "name: Validate with Hassfest\n\non:\n  push:\n    branches:\n      - master\n\n  pull_request:\n    branches: [\"*\"]\n\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  validate_hassfest:\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - uses: \"actions/checkout@v5\"\n      - uses: home-assistant/actions/hassfest@master\n"
  },
  {
    "path": ".github/workflows/linting.yaml",
    "content": "name: Linting\n\non:\n  push:\n    branches:\n      - master\n\n  pull_request:\n    branches: [\"*\"]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n\n      - name: Setup Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: 3.13\n\n      - name: Install dependencies\n        run: |\n          pip install -r requirements.txt\n      - name: flake8\n        run: flake8 .\n      - name: isort\n        run: isort --diff --check .\n      - name: Black\n        run: black --line-length 88 --diff --check .\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: \"Release\"\n\non:\n  release:\n    types:\n      - \"published\"\n\npermissions: {}\n\njobs:\n  release:\n    name: \"Release\"\n    runs-on: \"ubuntu-latest\"\n    permissions:\n      contents: write\n    steps:\n      - name: \"Checkout the repository\"\n        uses: \"actions/checkout@v5\"\n\n      - name: \"ZIP the integration directory\"\n        shell: \"bash\"\n        run: |\n          cd \"${{ github.workspace }}/custom_components/samsungtv_smart\"\n          zip samsungtv_smart.zip -r ./\n\n      - name: \"Upload the ZIP file to the release\"\n        uses: \"softprops/action-gh-release@v2.0.8\"\n        with:\n          files: ${{ github.workspace }}/custom_components/samsungtv_smart/samsungtv_smart.zip\n"
  },
  {
    "path": ".github/workflows/stale.yaml",
    "content": "# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.\n#\n# You can adjust the behavior by modifying this file.\n# For more information, see:\n# https://github.com/actions/stale\nname: \"Close stale issues and PRs\"\n\non:\n  schedule:\n  - cron: \"0 2 * * *\"\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  stale:\n    permissions:\n      issues: write  # for actions/stale to close stale issues\n      pull-requests: write  # for actions/stale to close stale PRs\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/stale@v10\n      with:\n        stale-issue-message: 'This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 7 days.'\n        stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 10 days.'\n        close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'\n        close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.'\n        exempt-issue-labels: 'Feature Request,documentation'\n        days-before-issue-stale: 45\n        days-before-pr-stale: -1\n        days-before-issue-close: 7\n        days-before-pr-close: -1\n        ascending: true\n        operations-per-run: 400\n"
  },
  {
    "path": ".github/workflows/validate.yaml",
    "content": "name: Validate with Hacs\n\non:\n  push:\n    branches:\n      - master\n\n  pull_request:\n    branches: [\"*\"]\n\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  validate_hacs:\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - uses: \"actions/checkout@v5\"\n      - name: HACS validation\n        uses: \"hacs/action@main\"\n        with:\n          category: \"integration\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# artifacts\n__pycache__\n.pytest*\n.cache\n*.egg-info\n*/build/*\n*/dist/*\n\n# pycharm\n.idea/\n\n# Unit test / coverage reports\n.coverage\ncoverage.xml\n\n# Home Assistant configuration\nconfig/*\n!config/configuration.yaml"
  },
  {
    "path": ".prettierignore",
    "content": "*.md\n.strict-typing\nazure-*.yml\ndocs/source/_templates/*\n"
  },
  {
    "path": ".pylintrc",
    "content": "[MESSAGES CONTROL]\n# PyLint message control settings\n# Reasons disabled:\n# format - handled by black\n# locally-disabled - it spams too much\n# duplicate-code - unavoidable\n# cyclic-import - doesn't test if both import on load\n# abstract-class-little-used - prevents from setting right foundation\n# unused-argument - generic callbacks and setup methods create a lot of warnings\n# too-many-* - are not enforced for the sake of readability\n# too-few-* - same as too-many-*\n# abstract-method - with intro of async there are always methods missing\n# inconsistent-return-statements - doesn't handle raise\n# too-many-ancestors - it's too strict.\n# wrong-import-order - isort guards this\n# consider-using-f-string - str.format sometimes more readable\n# ---\n# Enable once current issues are fixed:\n# consider-using-namedtuple-or-dataclass (Pylint CodeStyle extension)\n# consider-using-assignment-expr (Pylint CodeStyle extension)\ndisable =\n    format,\n    abstract-method,\n    cyclic-import,\n    duplicate-code,\n    inconsistent-return-statements,\n    locally-disabled,\n    not-context-manager,\n    too-few-public-methods,\n    too-many-ancestors,\n    too-many-arguments,\n    too-many-branches,\n    too-many-instance-attributes,\n    too-many-lines,\n    too-many-locals,\n    too-many-public-methods,\n    too-many-return-statements,\n    too-many-statements,\n    too-many-boolean-expressions,\n    unused-argument,\n    wrong-import-order,\n    consider-using-f-string,\n    unexpected-keyword-arg\n#    consider-using-namedtuple-or-dataclass,\n#    consider-using-assignment-expr\n"
  },
  {
    "path": ".ruff.toml",
    "content": "# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml\n\ntarget-version = \"py310\"\n\nselect = [\n    \"B007\", # Loop control variable {name} not used within loop body\n    \"B014\", # Exception handler with duplicate exception\n    \"C\",  # complexity\n    \"D\",  # docstrings\n    \"E\",  # pycodestyle\n    \"F\",  # pyflakes/autoflake\n    \"ICN001\", # import concentions; {name} should be imported as {asname}\n    \"PGH004\",  # Use specific rule codes when using noqa\n    \"PLC0414\", # Useless import alias. Import alias does not rename original package.\n    \"SIM105\", # Use contextlib.suppress({exception}) instead of try-except-pass\n    \"SIM117\", # Merge with-statements that use the same scope\n    \"SIM118\", # Use {key} in {dict} instead of {key} in {dict}.keys()\n    \"SIM201\", # Use {left} != {right} instead of not {left} == {right}\n    \"SIM212\", # Use {a} if {a} else {b} instead of {b} if not {a} else {a}\n    \"SIM300\", # Yoda conditions. Use 'age == 42' instead of '42 == age'.\n    \"SIM401\", # Use get from dict with default instead of an if block\n    \"T20\",  # flake8-print\n    \"TRY004\", # Prefer TypeError exception for invalid type\n    \"RUF006\", # Store a reference to the return value of asyncio.create_task\n    \"UP\",  # pyupgrade\n    \"W\",  # pycodestyle\n]\n\nignore = [\n    \"D202\",  # No blank lines allowed after function docstring\n    \"D203\",  # 1 blank line required before class docstring\n    \"D213\",  # Multi-line docstring summary should start at the second line\n    \"D404\",  # First word of the docstring should not be This\n    \"D406\",  # Section name should end with a newline\n    \"D407\",  # Section name underlining\n    \"D411\",  # Missing blank line before section\n    \"E501\",  # line too long\n    \"E731\",  # do not assign a lambda expression, use a def\n]\n\n[flake8-pytest-style]\nfixture-parentheses = false\n\n[pyupgrade]\nkeep-runtime-typing = true\n\n[mccabe]\nmax-complexity = 25"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"esbenp.prettier-vscode\", \"ms-python.python\"]\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            // Example of attaching to local debug server\n            \"name\": \"Python: Attach Local\",\n            \"type\": \"python\",\n            \"request\": \"attach\",\n            \"port\": 5678,\n            \"host\": \"localhost\",\n            \"pathMappings\": [\n                {\n                    \"localRoot\": \"${workspaceFolder}\",\n                    \"remoteRoot\": \".\"\n                }\n            ],\n        },\n        {\n            // Example of attaching to my production server\n            \"name\": \"Python: Attach Remote\",\n            \"type\": \"python\",\n            \"request\": \"attach\",\n            \"port\": 5678,\n            \"host\": \"homeassistant.local\",\n            \"pathMappings\": [\n                {\n                    \"localRoot\": \"${workspaceFolder}\",\n                    \"remoteRoot\": \"/usr/src/homeassistant\"\n                }\n            ],\n        }\n    ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  //\"editor.formatOnSave\": true\n  \"[python]\": {\n    \"editor.defaultFormatter\": \"ms-python.black-formatter\",\n    \"editor.formatOnSave\": true\n  },\n  // Added --no-cov to work around TypeError: message must be set\n  // https://github.com/microsoft/vscode-python/issues/14067\n  \"python.testing.pytestArgs\": [\"--no-cov\"],\n  // https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings\n  \"python.testing.pytestEnabled\": false\n}\n"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"Run Home Assistant on port 8123\",\n            \"type\": \"shell\",\n            \"command\": \"scripts/develop\",\n            \"problemMatcher\": []\n        },\n\t\t{\n\t\t  \"label\": \"Install Requirements\",\n\t\t  \"type\": \"shell\",\n\t\t  \"command\": \"pip3 install --use-deprecated=legacy-resolver -r requirements.txt\",\n\t\t  \"group\": {\n\t\t\t\"kind\": \"build\",\n\t\t\t\"isDefault\": true\n\t\t  },\n\t\t  \"presentation\": {\n\t\t\t\"reveal\": \"always\",\n\t\t\t\"panel\": \"new\"\n\t\t  },\n\t\t  \"problemMatcher\": []\n\t\t},\n\t\t{\n\t\t  \"label\": \"Install Test Requirements\",\n\t\t  \"type\": \"shell\",\n\t\t  \"command\": \"pip3 install --use-deprecated=legacy-resolver -r requirements_test.txt\",\n\t\t  \"group\": {\n\t\t\t\"kind\": \"build\",\n\t\t\t\"isDefault\": true\n\t\t  },\n\t\t  \"presentation\": {\n\t\t\t\"reveal\": \"always\",\n\t\t\t\"panel\": \"new\"\n\t\t  },\n\t\t  \"problemMatcher\": []\n\t\t},\n\t\t{\n\t\t  \"label\": \"Run PyTest\",\n\t\t  \"detail\": \"Run pytest for integration.\",\n\t\t  \"type\": \"shell\",\n\t\t  \"command\": \"pytest --cov-report term-missing -vv --durations=10\",\n\t\t  \"group\": {\n\t\t\t\"kind\": \"test\",\n\t\t\t\"isDefault\": true\n\t\t  },\n\t\t  \"presentation\": {\n\t\t\t\"reveal\": \"always\",\n\t\t\t\"panel\": \"new\"\n\t\t  },\n\t\t  \"problemMatcher\": []\n\t\t}\n    ]\n}\n"
  },
  {
    "path": "Dockerfile.dev",
    "content": "FROM mcr.microsoft.com/devcontainers/python:1-3.13\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# Uninstall pre-installed formatting and linting tools\n# They would conflict with our pinned versions\nRUN \\\n    pipx uninstall pydocstyle \\\n    && pipx uninstall pycodestyle \\\n    && pipx uninstall mypy \\\n    && pipx uninstall pylint\n\nRUN \\\n    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \\\n    && apt-get update \\\n    && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \\\n        # Additional library needed by some tests and accordingly by VScode Tests Discovery\n        bluez \\\n        ffmpeg \\\n        libudev-dev \\\n        libavformat-dev \\\n        libavcodec-dev \\\n        libavdevice-dev \\\n        libavutil-dev \\\n        libgammu-dev \\\n        libswscale-dev \\\n        libswresample-dev \\\n        libavfilter-dev \\\n        libpcap-dev \\\n        libturbojpeg0 \\\n        libyaml-dev \\\n        libxml2 \\\n        git \\\n        cmake \\\n        autoconf \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Add go2rtc binary\nCOPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc\n\n# Install uv\nRUN pip3 install uv\n\nWORKDIR /workspaces\n\n# Set the default shell to bash instead of sh\nENV SHELL /bin/bash\n"
  },
  {
    "path": "LICENSE",
    "content": "Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "[![](https://img.shields.io/github/release/ollo69/ha-samsungtv-smart/all.svg?style=for-the-badge)](https://github.com/ollo69/ha-samsungtv-smart/releases)\n[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs)\n[![](https://img.shields.io/github/license/ollo69/ha-samsungtv-smart?style=for-the-badge)](LICENSE)\n[![](https://img.shields.io/badge/MAINTAINER-%40ollo69-red?style=for-the-badge)](https://github.com/ollo69)\n[![](https://img.shields.io/badge/COMMUNITY-FORUM-success?style=for-the-badge)](https://community.home-assistant.io)\n\n# HomeAssistant - SamsungTV Smart Component\n\nThis is a custom component to allow control of SamsungTV devices in [HomeAssistant](https://home-assistant.io).\nIs a modified version of the built-in [samsungtv](https://www.home-assistant.io/integrations/samsungtv/) with some extra\n features.<br/>\n**This plugin is only for 2016+ TVs model!** (maybe all tizen family)\n\nThis project is a fork of the component [SamsungTV Tizen](https://github.com/jaruba/ha-samsungtv-tizen). I added some\nfeature like the possibility to configure it using the HA user interface, simplifing the configuration process.\nI also added some code optimizition in the comunication layer using async aiohttp instead of request.\n**Part of the code and documentation available here come from the original project.**<br/>\n\n# Additional Features:\n\n* Ability to send keys using a native Home Assistant service\n* Ability to send chained key commands using a native Home Assistant service\n* Supports Assistant commands (Google Home, should work with Alexa too, but untested)\n* Extended volume control\n* Ability to customize source list at media player dropdown list\n* Cast video URLs to Samsung TV\n* Connect to SmartThings Cloud API for additional features: see TV channel names, see which HDMI source is selected, more key codes to change input source\n* Display logos of TV channels (requires Smartthings enabled) and apps\n\n![N|Solid](https://i.imgur.com/8mCGZoO.png)\n![N|Solid](https://i.imgur.com/t3e4bJB.png)\n\n# Installation\n\n### 1. Easy Mode\n\nInstall via HACS.\n\n### 2. Manual\n\nInstall it as you would do with any homeassistant custom component:\n\n1. Download `custom_components` folder.\n1. Copy the `samsungtv_smart` directory within the `custom_components` directory of your homeassistant installation. The `custom_components` directory resides within your homeassistant configuration directory.\n**Note**: if the custom_components directory does not exist, you need to create it.\nAfter a correct installation, your configuration directory should look like the following.\n    ```\n    └── ...\n    └── configuration.yaml\n    └── custom_components\n        └── samsungtv_smart\n            └── __init__.py\n            └── media_player.py\n            └── websockets.py\n            └── shortcuts.py\n            └── smartthings.py\n            └── upnp.py\n            └── exceptions.py\n            └── ...\n    ```\n\n# Configuration\n\nOnce the component has been installed, you need to configure it in order to make it work.\nThere are two ways of doing so:\n- Using the web interface (Lovelace) [**recommended**]\n- Manually editing the `configuration.yaml` file\n\n**Important**: To complete the configuration procedure properly, you must be sure that your **TV is turned on and\nconnected to the LAN (wired or wireless)**. Stay near to your TV during configuration because probably you will need\nto accept the access request that will prompt on your TV screen.\n\n**Note**: To configure the component for using **SmartThings (strongly suggested)** you need to generate an access\ntoken as explained in [this guide](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md).\nAlso make sure your **TV is logged into your SmartThings account** and **registered in SmartThings phone app** before\nstarting configuration.\n\n### Option A: Configuration using the web UI [**recommended**]\n\n1. From the Home Assistant front-end, navigate to 'Configuration' then 'Integrations'. Click `+` button in botton right corner,\nsearch '**SamsungTV Smart**' and click 'Configure'.\n2. In the configuration mask, enter the IP address of the TV, the name for the Entity and the SmartThings personal\naccess token (if created) and then click 'Submit'\n3. **Important**: look for your TV screen and confirm **immediately** with OK if a popup appear.\n4. Congrats! You're all set!\n\n**Note**: be sure that your TV and HA are connected to the same VLAN. Websocket connection through different VLAN normally\nnot work because not supported by Samsung TV.\nIf you have errors during configuration, try to power cycle your TV. This will close running applications that can prevent\nwebsocket connection initialization.\n\n### Option B: Configuration via editing `configuration.yaml`\n\n**From v0.3.16 initial configuration from yaml is not allowed.**<br>\nYou can still use `configuration.yaml` to set the additional parameter as explained below.\n\n## Configuration options\n\nFrom the Home Assistant front-end, navigate to 'Configuration' then 'Integrations'. Identify the '**SamsungTV Smart**'\nintegration configured for your TV and click the `OPTIONS` button.<br/>\nHere you can change the following options:\n\n- **Use SmartThings TV Status information**<br/>\n(default = True)<br/>\n**This option is available only if SmartThings is configured.**\nWhen enabled the component will try to retrieve from SmartThings the information\nabout the TV Status. This information is always used in conjunction with local ping result.<br/>\n\n- **Use SmartThings TV Channels information**<br/>\n(default = True)<br/>\n**This option is available only if SmartThings is configured.**\nWhen enabled the component will try to retrieve from SmartThings the information about the TV Channel\nand TV Channel Name or the Running App<br/>\n**Note: in some case this information is not properly updated, disabled it you have incorrect information.**<br/>\n\n- **Use SmartThings TV Channels number information**<br/>\n(default = False)<br/>\n**This option is available only if SmartThings is configured.**\nWhen enabled then the TV Channel Names will show as media titles, by setting this to True the\nTV Channel Number will also be attached to the end of the media title (when applicable).<br/>\n**Note: not always SmartThings provide the information for channel_name and channel_number.**<br/>\n\n- **Logo options**<br/>\nThe background color and channel / service logo preference to use, example: \"white-color\" (background: white, logo: color).<br/>\nSupported values: \"none\", \"white-color\", \"dark-white\", \"blue-color\", \"blue-white\", \"darkblue-white\", \"transparent-color\", \"transparent-white\"<br/>\nDefault value: \"white-color\" (background: white, logo: color)<br/>\nNotice that your logo is missing or outdated? In case of a missing TV channel logo also make sure you have Smartthings enabled.\nThis is required for the component to know the name of the TV channel.<br/>\nCheck guide [here](https://github.com/jaruba/ha-samsungtv-tizen/blob/master/Logos.md)\nfor updating the logo database this component is relying on.\n\n- **Allow use of local logo images**<br/>\n(default = True)<br/>\nWhen enabled the integration will try to get logo image for the current media from the `www/samsungtv_smart_logos` sub folder of home-assistant configuration folder.\nYou can add new logo images in this folder, using the following rules for logo filename:\n  - must be equal to the name of the `media_title` attribute, removing space, `_` and `.` characters and replacing `+` character with\n  the string `plus`\n  - must have the `.png` suffix\n  - must be in `png` format (suggested size is 400x400 pixels)\n\n- **Applications list load mode at startup**<br/>\nPossible values: `All Apps`, `Default Apps` and `Not Load`<br/>\nThis option determine the mode application list is automatic generated.<br>\nWith `All Apps` the list will contain all apps installed on the TV, with `Default Apps` will be generated a minimal list\nwith only the most common application, with `Not Load` application list will be empty.<br/>\n**Note: If a custom `Application List` in config options is defined this option is not available.**<br>\n\n- **Method used to turn on TV**<br/>\nPossible values: `WOL Packet` and `SmartThings`<br/>\n**This option is available only if SmartThings is configured.**\nWOL Packet is better when TV use wired connection.<br/>\nSmartThings normally work only when TV use wireless connection.<br/>\n\n- **Show advanced options**<br/>\nSelecting this option and clicking submit a new options menu is opened containing the list of other options described below.\n\n### Advanced options\n\n- **Applications launch method used**<br/>\nPossible values: `Control Web Socket Channel`, `Remote Web Socket Channel` and `Rest API Call`<br/>\nThis option determine the mode used to launch applications.<br/>\nUse `Rest API Call` only if the other 2 methods do not work.<br/>\n\n- **Number of times WOL packet is sent to turn on TV**<br/>\n(default = 1, range from 1 to 5)<br/>\nThis option allow to configure the number of time the WOL packet is sent to turn on TV. Increase the value\nuntil the TV properly turn-on.<br/>\n\n- **TCP port used to check power status**<br/>\n(default = 0, range from 0 to 65535)<br/>\nWith this option is possible to check the availability of a specific port to determinate power status instead\nof using ICMP echo. To continue use ICMP echo, leave the value to `0`, otherwise set a port that is known becoming\navailable when TV is on (possible working ports, depending on TV models, are `9110`, `9119`, `9197`).</br>\n**N.B. If you set an invalid port here, TV is always reported as `off`.**</br>\n\n- **Binary sensor to help detect power status**<br/>\nAn external `binary_sensor` selectable from a list that can be used to determinate TV power status.<br/>\nThis can be any available `binary_sensor` that can better determinate the status of the TV, for example a\n`binary_sensor` based on TV power consumption. It is suggested to not use a sensor based on `ping` platform\nbecause this method is already implemented by the integration.</br>\n\n- **Use volume mute status to detect fake power ON**<br/>\n(default = True)<br/>\nWhen enabled try to detect fake power on based on the Volume mute state, based on the assumption that when the\nTV is powered on the volume is always unmuted.<br/>\n\n- **Dump apps list on log file at startup**<br/>\n(default = False)<br/>\nWhen enabled the component will try to dump the list of available apps on TV in the HA log file at Info level.\nThe dump of the apps may not work for some TV models.<br/>\n\n- **Power button switch to art mode**<br/>\n(default = False)<br/>\nWhen enabled the power button in UI will be used to toggle from `On` to `Art Mode` (and vice versa) and will not\npower off the TV (you can still use the `turn off` service to power off the TV).<br/>\n**Note: This option is valid only for TV that support `Art Mode` (\"The Frame\" models).**<br>\n\n### Synched entities configuration\n\n- **List of entity to Power OFF with TV**<br/>\nA list of HA entity to Turn OFF when the TV entity is turned OFF (maximum 4). Select entities from list.\nThis call the service `homeassistant.turn_off` for maximum the first 4 entity in the provided list.<br/>\n\n- **List of entity to Power ON with TV**<br/>\nA list of HA entity to Turn ON when the TV entity is turned ON (maximum 4).  Select entities from list.\nThis call the service `homeassistant.turn_on` for maximum the first 4 entity in the provided list.<br/>\n\n### Sources list configuration\n\nThis contains the KEYS visible sources in the dropdown list in media player UI.<br/>\nYou can configure the pair list `Name: Key` using the yaml editor in the option page. If a source list is present in\n`configuration.yaml`, it will be imported in the options the first time that the integration is loaded.<br/>\n\nDefault value:<br/>\n```\n    1| TV: KEY_TV\n    2| HDMI: KEY_HDMI\n```\n\nIf SmartThings is [configured](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md) and the\nsource_list not, the component will try to identify and configure automatically the sources configured on the TV with\nthe relative associated names (new feature, tested on QLed TV). The created list is available in the HA log file.<br/>\nYou can also chain KEYS, example:\n```\n    1| TV: KEY_SOURCES+KEY_ENTER\n```\n\nAnd even add delays (in milliseconds) between sending KEYS, example:<br/>\n```\n    1| TV: KEY_SOURCES+500+KEY_ENTER\n```\n\nResources: [key codes](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Key_codes.md) / [key patterns](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Key_chaining.md)<br/>\n**Warning: changing input source with voice commands only works if you set the device name in `source_list` as one of\nthe whitelisted words that can be seen on [this page](https://web.archive.org/web/20181218120801/https://developers.google.com/actions/reference/smarthome/traits/modes#mode-settings)\n(under \"Mode Settings\")**<br/>\n\n### Application list configuration\n\nThis contains the APPS visible sources in the dropdown list in media player UI.<br/>\nYou can configure the pair list `Name: Key` using the yaml editor in the option page. If an application list is present in\n`configuration.yaml`, it will be imported in the options the first time that the integration is loaded.<br/>\n\nIf the `Application list` is not manually configured, during startup the integration will try to automatically generate a list\nof available application and a log message is generated with the content of the list. This list can be used to create a manual\nlist following [app_list guide](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/App_list.md). Automatic list\ngeneration not work with some TV models.<br/>\n\nExample value:\n```\n    1| Netflix: \"11101200001\"\n    2| YouTube: \"111299001912\"\n    3| Spotify: \"3201606009684\"\n```\n\nKnown lists of App IDs: [List 1](https://github.com/tavicu/homebridge-samsung-tizen/issues/26#issuecomment-447424879),\n[List 2](https://github.com/Ape/samsungctl/issues/75#issuecomment-404941201)<br/>\n\n### Channel list configuration\n\nThis contains the tv CHANNELS visible sources in the dropdown list in media player UI. To guarantee performance keep the list small,\nrecommended maximum 30 channels.<br/>\nYou can configure the pair list `Name: Key` using the yaml editor in the option page. If a channel list is present in\n`configuration.yaml`, it will be imported in the options the first time that the integration is loaded.<br/>\n\nExample value:\n```\n    1| MTV: \"14\"\n    2| Eurosport: \"20\"\n    3| TLC: \"21\"\n```\n\nYou can also specify the source that must be used for every channel. The source must be one of the source name defined in the `source_list`<br/>\nExample value:\n```\n    1| MTV: 14@TV\n    2| Eurosport: 20@TV\n    3| TLC: 21@HDMI\n```\n\n## Custom configuration parameters\n\nYou can configure additional option for the component using configuration variable in `configuration.yaml` section.<br/>\n\nSection in `configuration.yaml` file can also not be present and is not required for component to work. If you\nwant to configure any parameters, you must create one section that start with `- host` as shown in the example below:<br/>\n```\nsamsungtv_smart:\n  - host: <YOUR TV IP ADDRES>\n    ...\n```\nThen you can add any of the following parameters:<br/>\n\n- **mac:**<br/>\n(string)(Optional)<br/>\nThis is an optional value, normally is automatically detected during setup phase and so is not required to specify it.\nYou should try to configure this parameter only if the setup fail in the detection.<br/>\nThe mac-address is used to turn on the TV. If you set it manually, you must find the right value from the TV Menu or\nfrom your network router.<br/>\n\n- **broadcast_address:**<br/>\n(string)(Optional)<br/>\n**Do not set this option if you do not know what it does, it can break turning your TV on.**<br/>\nThe ip address of the host to send the magic packet (for wakeonlan) to if the \"mac\" property is also set.<br/>\nDefault value: \"255.255.255.255\"<br/>\nExample value: \"192.168.1.255\"<br/>\n\n### Deprecated configuration parameters\n\nDeprecated parameters were used by old integration version. Are still valid but normally are automatically imported\nin application options and not used anymore, so after first import can be removed from `configuration.yaml`.\n\n- **source_list:**<br/>\n(json)(Optional)<br/>\nThis contains the KEYS visible sources in the dropdown list in media player UI.<br/>\nDefault value: '{\"TV\": \"KEY_TV\", \"HDMI\": \"KEY_HDMI\"}'<br/>\nIf SmartThings is [configured](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md) and the\nsource_list not, the component will try to identify and configure automatically the sources configured on the TV with\nthe relative associated names (new feature, tested on QLed TV). The created list is available in the HA log file.<br/>\nYou can also chain KEYS, example: '{\"TV\": \"KEY_SOURCES+KEY_ENTER\"}'<br/>\nAnd even add delays (in milliseconds) between sending KEYS, example:<br/>\n    '{\"TV\": \"KEY_SOURCES+500+KEY_ENTER\"}'<br/>\nResources: [key codes](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Key_codes.md) / [key patterns](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Key_chaining.md)<br/>\n**Warning: changing input source with voice commands only works if you set the device name in `source_list` as one of\nthe whitelisted words that can be seen on [this page](https://web.archive.org/web/20181218120801/https://developers.google.com/actions/reference/smarthome/traits/modes#mode-settings)\n(under \"Mode Settings\")**<br/>\n\n- **app_list:**<br/>\n(json)(Optional)<br/>\nThis contains the APPS visible sources in the dropdown list in media player UI.<br/>\nDefault value: AUTOGENERATED<br/>\nIf the `app_list` is not manually configured, during startup is generated a file in the custom component folder with the\nlist of all available applications. This list can be used to create a manual list following [app_list guide](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/App_list.md)<br/>\nExample value: '{\"Netflix\": \"11101200001\", \"YouTube\": \"111299001912\", \"Spotify\": \"3201606009684\"}'<br/>\nKnown lists of App IDs: [List 1](https://github.com/tavicu/homebridge-samsung-tizen/issues/26#issuecomment-447424879),\n[List 2](https://github.com/Ape/samsungctl/issues/75#issuecomment-404941201)<br/>\n\n- **channel_list:**<br/>\n(json)(Optional)<br/>\nThis contains the tv CHANNELS visible sources in the dropdown list in media player UI. To guarantee performance keep the list small,\nrecommended maximum 30 channels.<br/>\nExample value: '{\"MTV\": \"14\", \"Eurosport\": \"20\", \"TLC\": \"21\"}'<br/>\nYou can also specify the source that must be used for every channel. The source must be one of the defined in the `source_list`<br/>\nExample value: '{\"MTV\": \"14@TV\", \"Eurosport\": \"20@TV\", \"TLC\": \"21@HDMI\"}'<br/>\n\n\n### Removed configuration parameters\n\nRemoved parameters were used by old integration version, are not used and supported anymore and replaced by application option.\nFor this reason should be removed from `configuration.yaml`.\n\n- **api_key:**<br/>\n(string)(Optional) (obsolete/not used from v0.3.16 - configuration from yaml is not allowed)<br/>\nAPI Key for the SmartThings Cloud API, this is optional but adds better state handling on, off, channel name, hdmi source,\nand a few new keys: `ST_TV`, `ST_HDMI1`, `ST_HDMI2`, `ST_HDMI3`, etc. (see more at [SmartThings Keys](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md#smartthings-keys))<br/>\nRead [How to get an API Key for SmartThings](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md)<br/>\nThis parameter can also be provided during the component configuration using the user interface.<br/>\n**Note: this parameter is used only during initial configuration and then stored in the registry. It's not possible to change the value after that the component is configured. To change the value you must delete the integration from UI.**<br/>\n\n- **device_id:**<br/>\n(string)(Optional) (obsolete/not used from v0.3.16 - configuration from yaml is not allowed)<br/>\nDevice ID for the SmartThings Cloud API. This is optional, to be used only if component fails to automatically determinate it.\nRead [SmartThings Device ID](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md#smartthings-device-id)\nto understand how identify the correct value to use.<br/>\nThis parameter will be requested during component configuration from user interface when required.<br/>\n**Note: this parameter is used only during initial configuration and then stored in the registry. It's not possible to\nchange the value after that the component is configured. To change the value you must delete the integration from UI.**<br/>\n\n- **device_name:** (obsolete/not used from v0.3.16 - configuration from yaml is not allowed)<br/>\n(string)(Optional)<br/>\nThis is an optional value, used only to identify the TV in SmartThings during initial configuration if you have more TV\nregistered. You should  configure this parameter only if the setup fails in the detection.<br/>\nThe device_name to use can be read using the SmartThings app<br/>\n**Note: this parameter is used only during initial configuration.**<br/>\n\n- **show_channel_number:** (obsolete/not used from v0.3.16 and replaced by Configuration options)<br/>\n(boolean)(Optional)<br/>\nIf the SmartThings API is enabled (by settings \"api_key\" and \"device_id\"), then the TV Channel Names will show as media\ntitles, by setting this to True the TV Channel Number will also be attached to the end of the media title (when applicable).<br/>\n**Note: not always SmartThings provide the information for channel_name and channel_number.**<br/>\n\n- **load_all_apps:** (obsolete/not used from v0.3.4 and replaced by Configuration options)<br/>\n(boolean)(Optional)<br/>\nThis option is `True` by default.</br>\nSetting this parameter to false, if a custom `app_list` is not defined, the automatic app_list will be generated\nlimited to few application (the most common).<br/>\n\n- **update_method:** (obsolete/not used from v0.3.3)<br/>\n(string)(Optional)<br/>\nThis change the ping method used for state update. Values: \"ping\", \"websockets\" and \"smartthings\"<br/>\nDefault value: \"ping\" if SmartThings is not enabled, else \"smartthings\"<br/>\nExample update_method: \"websockets\"<br/>\n\n- **update_custom_ping_url:** (obsolete/not used from v0.2.x)<br/>\n(string)(Optional)<br/>\nUse custom endpoint to ping.<br/>\nDefault value: PING TO 8001 ENDPOINT<br/>\nExample update_custom_ping_url: \"http://192.168.1.77:9197/dmr\"<br/>\n\n- **scan_app_http:** (obsolete/not used from v0.2.x)<br/>\n(boolean)(Optional)<br/>\nThis option is `True` by default. In some cases (if numerical IDs are used when setting `app_list`) HTTP polling will\nbe used (1 request per app) to get the running app.<br/>\nThis is a lengthy task that some may want to disable, you can do so by setting this option to `False`.<br/>\nFor more information about how we get the running app, read the [app_list guide](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/App_list.md).<br/>\n\n# Usage\n\n### Known Supported Voice Commands\n\n* Turn on `SAMSUNG-TV-NAME-HERE` (for some older TVs this only works if the TV is connected by LAN cable to the Network)\n* Turn off `SAMSUNG-TV-NAME-HERE`\n* Volume up on `SAMSUNG-TV-NAME-HERE` (increases volume by 1)\n* Volume down on `SAMSUNG-TV-NAME-HERE` (decreases volume by 1)\n* Set volume to 50 on `SAMSUNG-TV-NAME-HERE` (sets volume to 50 out of 100)\n* Mute `SAMSUNG-TV-NAME-HERE` (sets volume to 0)\n* Change input source to `DEVICE-NAME-HERE` on `SAMSUNG-TV-NAME-HERE` (only works if `DEVICE-NAME-HERE` is a whitelisted word from [this page](https://web.archive.org/web/20181218120801/https://developers.google.com/actions/reference/smarthome/traits/modes) under \"Mode Settings\")\n\n(if you find more supported voice commands, please create an issue so I can add them here)\n\n### Cast to TV\n\n```\nservice: media_player.play_media\n```\n\n```json\n{\n  \"entity_id\": \"media_player.samsungtv\",\n  \"media_content_type\": \"url\",\n  \"media_content_id\": \"FILE_URL\",\n}\n```\nReplace `FILE_URL` with the url of your file\n\n### Cast to YouTube\n\n```\nservice: media_player.play_media\n```\n\n```json\n{\n  \"entity_id\": \"media_player.samsungtv\",\n  \"media_content_type\": \"url\",\n  \"media_content_id\": \"YOUTUBE_URL\",\n  \"enqueue\": \"play\",\n}\n```\nReplace `YOUTUBE_URL` with the url of the video you want to play\nAll 4 enqueue modes are supported. Shorts videos URL are also supported.\n**Note**: `enqueue` is required, or the service will open the video using TV Web Browser.\n\n### Send Keys\n\n```\nservice: media_player.play_media\n```\n\n```json\n{\n  \"entity_id\": \"media_player.samsungtv\",\n  \"media_content_type\": \"send_key\",\n  \"media_content_id\": \"KEY_CODE\"\n}\n```\n**Note**: Change \"KEY_CODE\" by desired [key_code](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Key_codes.md). (also works with key chaining and SmartThings keys: ST_TV, ST_HDMI1, ST_HDMI2, ST_HDMI3, etc. / see more at [SmartThings Keys](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md#smartthings-keys))\n\nScript example:\n```\ntv_channel_down:\n  alias: Channel down\n  sequence:\n  - service: media_player.play_media\n    data:\n      entity_id: media_player.samsung_tv55\n      media_content_type: \"send_key\"\n      media_content_id: KEY_CHDOWN\n```\n\n### Hold Keys\n```\nservice: media_player.play_media\n```\n\n```json\n{\n  \"entity_id\": \"media_player.samsungtv\",\n  \"media_content_type\": \"send_key\",\n  \"media_content_id\": \"KEY_CODE, <hold_time>\"\n}\n```\n\n**Note**: Change \"KEY_CODE\" by desired [key_code](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Key_codes.md) and <hold_time> with a valid numeric value in milliseconds (this also works with key chaining but not with SmartThings keys).\n\n***Key Chaining Patterns***\n---------------\nKey chaining is also supported, which means a pattern of keys can be set by delimiting the keys with the \"+\" symbol, delays can also be set in milliseconds between the \"+\" symbols.\n\n[See the list of known Key Chaining Patterns](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Key_chaining.md)\n\n\n***Open Browser Page***\n---------------\n\n```\nservice: media_player.play_media\n```\n\n```json\n{\n  \"entity_id\": \"media_player.samsungtv\",\n  \"media_content_type\": \"browser\",\n  \"media_content_id\": \"https://www.google.com\"\n}\n```\n\n***Send Text***\n---------------\nTo send a specific text to a selected text input\n\n```\nservice: media_player.play_media\n```\n\n```json\n{\n  \"entity_id\": \"media_player.samsungtv\",\n  \"media_content_type\": \"send_text\",\n  \"media_content_id\": \"your text\"\n}\n```\n\n***Select sound mode (SmartThings only)***\n---------------\n\n```\nservice: media_player.select_sound_mode\n```\n\n```json\n{\n  \"entity_id\": \"media_player.samsungtv\",\n  \"sound_mode\": \"your mode\"\n}\n```\n\n**Note**: You can get list of valid `sound_mode` in the `sound_mode_list` state attribute\n\n\n***Select picture mode (SmartThings only)***\n---------------\n\n```\nservice: samsungtv_smart.select_picture_mode\n```\n\n```json\n{\n  \"entity_id\": \"media_player.samsungtv\",\n  \"picture_mode\": \"your mode\"\n}\n```\n\n**Note**: You can get list of valid `picture_mode` in the `picture_mode_list` state attribute\n\n\n***Set Art Mode (for TV that support it)***\n---------------\n\n```\nservice: samsungtv_smart.set_art_mode\n```\n\n```json\n{\n  \"entity_id\": \"media_player.samsungtv\"\n}\n```\n\n# Be kind!\nIf you like the component, why don't you support me by buying me a coffe?\nIt would certainly motivate me to further improve this work.\n\n[![Buy me a coffe!](https://www.buymeacoffee.com/assets/img/custom_images/black_img.png)](https://www.buymeacoffee.com/ollo69)\n\nCredits\n-------\n\nThis integration is developed by [Ollo69][ollo69] based on integration [SamsungTV Tizen][samsungtv_tizen].<br/>\nOriginal SamsungTV Tizen integration was developed by [jaruba][jaruba].<br/>\nLogo support is based on [jaruba channels-logo][channels-logo] and was developed with the support of [Screwdgeh][Screwdgeh].<br/>\n\n[ollo69]: https://github.com/ollo69\n[samsungtv_tizen]: https://github.com/jaruba/ha-samsungtv-tizen\n[jaruba]: https://github.com/jaruba\n[Screwdgeh]: https://github.com/Screwdgeh\n[channels-logo]: https://github.com/jaruba/channel-logos\n"
  },
  {
    "path": "config/configuration.yaml",
    "content": "# Loads default set of integrations. Do not remove.\ndefault_config:\n\n# Load frontend themes from the themes folder\nfrontend:\n  themes: !include_dir_merge_named themes\n\n# Text to speech\ntts:\n  - platform: google_translate\n\nlogger:\n  default: info\n  #logs:\n  #  custom_components.samsungtv_smart: debug\n"
  },
  {
    "path": "custom_components/__init__.py",
    "content": "\"\"\"Custom components module.\"\"\"\n"
  },
  {
    "path": "custom_components/samsungtv_smart/__init__.py",
    "content": "\"\"\"The samsungtv_smart integration.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nimport os\nfrom pathlib import Path\nimport socket\n\nfrom aiohttp import ClientConnectionError, ClientResponseError, ClientSession\nimport async_timeout\nimport voluptuous as vol\nfrom websocket import WebSocketException\n\nfrom homeassistant.components.http import StaticPathConfig\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import (\n    ATTR_DEVICE_ID,\n    CONF_ACCESS_TOKEN,\n    CONF_API_KEY,\n    CONF_BROADCAST_ADDRESS,\n    CONF_DEVICE_ID,\n    CONF_HOST,\n    CONF_ID,\n    CONF_MAC,\n    CONF_NAME,\n    CONF_PORT,\n    CONF_TIMEOUT,\n    CONF_TOKEN,\n    MAJOR_VERSION,\n    MINOR_VERSION,\n    Platform,\n    __version__,\n)\nfrom homeassistant.core import HomeAssistant, callback\nimport homeassistant.helpers.config_validation as cv\nfrom homeassistant.helpers.dispatcher import async_dispatcher_send\nfrom homeassistant.helpers.storage import STORAGE_DIR\nfrom homeassistant.helpers.typing import ConfigType\n\nfrom .api.samsungws import ConnectionFailure, SamsungTVWS\nfrom .api.smartthings import SmartThingsTV\nfrom .const import (\n    ATTR_DEVICE_MAC,\n    ATTR_DEVICE_MODEL,\n    ATTR_DEVICE_NAME,\n    ATTR_DEVICE_OS,\n    CONF_APP_LIST,\n    CONF_CHANNEL_LIST,\n    CONF_DEVICE_NAME,\n    CONF_LOAD_ALL_APPS,\n    CONF_SCAN_APP_HTTP,\n    CONF_SHOW_CHANNEL_NR,\n    CONF_SOURCE_LIST,\n    CONF_ST_ENTRY_UNIQUE_ID,\n    CONF_SYNC_TURN_OFF,\n    CONF_SYNC_TURN_ON,\n    CONF_UPDATE_CUSTOM_PING_URL,\n    CONF_UPDATE_METHOD,\n    CONF_USE_ST_INT_API_KEY,\n    CONF_WS_NAME,\n    DATA_CFG,\n    DATA_CFG_YAML,\n    DATA_OPTIONS,\n    DEFAULT_PORT,\n    DEFAULT_SOURCE_LIST,\n    DEFAULT_TIMEOUT,\n    DOMAIN,\n    LOCAL_LOGO_PATH,\n    MIN_HA_MAJ_VER,\n    MIN_HA_MIN_VER,\n    RESULT_NOT_SUCCESSFUL,\n    RESULT_ST_DEVICE_NOT_FOUND,\n    RESULT_SUCCESS,\n    RESULT_WRONG_APIKEY,\n    SIGNAL_CONFIG_ENTITY,\n    WS_PREFIX,\n    __min_ha_version__,\n)\nfrom .logo import CUSTOM_IMAGE_BASE_URL, STATIC_IMAGE_BASE_URL\n\n# workaroud for failing import native domain when custom integration is present\ntry:\n    from homeassistant.components.smartthings.const import DOMAIN as ST_DOMAIN\nexcept ImportError:\n    ST_DOMAIN = \"smartthings\"\n\nDEVICE_INFO = {\n    ATTR_DEVICE_ID: \"id\",\n    ATTR_DEVICE_MAC: \"wifiMac\",\n    ATTR_DEVICE_NAME: \"name\",\n    ATTR_DEVICE_MODEL: \"modelName\",\n    ATTR_DEVICE_OS: \"OS\",\n}\n\nSAMSMART_PLATFORM = [Platform.MEDIA_PLAYER, Platform.REMOTE]\n\nSAMSMART_SCHEMA = {\n    vol.Optional(CONF_SOURCE_LIST, default=DEFAULT_SOURCE_LIST): cv.string,\n    vol.Optional(CONF_APP_LIST): cv.string,\n    vol.Optional(CONF_CHANNEL_LIST): cv.string,\n    vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,\n    vol.Optional(CONF_MAC): cv.string,\n    vol.Optional(CONF_BROADCAST_ADDRESS): cv.string,\n}\n\n\ndef ensure_unique_hosts(value):\n    \"\"\"Validate that all configs have a unique host.\"\"\"\n    vol.Schema(vol.Unique(\"duplicate host entries found\"))(\n        [socket.gethostbyname(entry[CONF_HOST]) for entry in value]\n    )\n    return value\n\n\nCONFIG_SCHEMA = vol.Schema(\n    {\n        DOMAIN: vol.All(\n            cv.ensure_list,\n            [\n                cv.deprecated(CONF_LOAD_ALL_APPS),\n                cv.deprecated(CONF_PORT),\n                cv.deprecated(CONF_UPDATE_METHOD),\n                cv.deprecated(CONF_UPDATE_CUSTOM_PING_URL),\n                cv.deprecated(CONF_SCAN_APP_HTTP),\n                vol.Schema(\n                    {\n                        vol.Required(CONF_HOST): cv.string,\n                        vol.Optional(CONF_NAME): cv.string,\n                        vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,\n                        vol.Optional(CONF_API_KEY): cv.string,\n                        vol.Optional(CONF_DEVICE_NAME): cv.string,\n                        vol.Optional(CONF_DEVICE_ID): cv.string,\n                        vol.Optional(CONF_LOAD_ALL_APPS, default=True): cv.boolean,\n                        vol.Optional(CONF_UPDATE_METHOD): cv.string,\n                        vol.Optional(CONF_UPDATE_CUSTOM_PING_URL): cv.string,\n                        vol.Optional(CONF_SCAN_APP_HTTP, default=True): cv.boolean,\n                        vol.Optional(CONF_SHOW_CHANNEL_NR, default=False): cv.boolean,\n                        vol.Optional(CONF_WS_NAME): cv.string,\n                    }\n                ).extend(SAMSMART_SCHEMA),\n            ],\n            ensure_unique_hosts,\n        )\n    },\n    extra=vol.ALLOW_EXTRA,\n)\n\n_LOGGER = logging.getLogger(__name__)\n\n\ndef tv_url(host: str, address: str = \"\") -> str:\n    \"\"\"Return url to the TV.\"\"\"\n    return f\"http://{host}:8001/api/v2/{address}\"\n\n\ndef is_min_ha_version(min_ha_major_ver: int, min_ha_minor_ver: int) -> bool:\n    \"\"\"Check if HA version at least a specific version.\"\"\"\n    return MAJOR_VERSION > min_ha_major_ver or (\n        MAJOR_VERSION == min_ha_major_ver and MINOR_VERSION >= min_ha_minor_ver\n    )\n\n\ndef is_valid_ha_version() -> bool:\n    \"\"\"Check if HA version is valid for this integration.\"\"\"\n    return is_min_ha_version(MIN_HA_MAJ_VER, MIN_HA_MIN_VER)\n\n\ndef _notify_message(\n    hass: HomeAssistant, notification_id: str, title: str, message: str\n) -> None:\n    \"\"\"Notify user with persistent notification.\"\"\"\n    hass.async_create_task(\n        hass.services.async_call(\n            domain=\"persistent_notification\",\n            service=\"create\",\n            service_data={\n                \"title\": title,\n                \"message\": message,\n                \"notification_id\": f\"{DOMAIN}.{notification_id}\",\n            },\n        )\n    )\n\n\ndef _load_option_list(src_list):\n    \"\"\"Load list parameters in JSON from configuration.yaml.\"\"\"\n\n    if src_list is None:\n        return None\n    if isinstance(src_list, dict):\n        return src_list\n\n    result = {}\n    try:\n        result = json.loads(src_list)\n    except TypeError:\n        _LOGGER.error(\"Invalid format parameter: %s\", str(src_list))\n    return result\n\n\ndef token_file_name(hostname: str) -> str:\n    \"\"\"Return token file name.\"\"\"\n    return f\"{DOMAIN}_{hostname}_token\"\n\n\ndef _remove_token_file(hass, hostname, token_file=None):\n    \"\"\"Try to remove token file.\"\"\"\n    if not token_file:\n        token_file = hass.config.path(STORAGE_DIR, token_file_name(hostname))\n\n    if os.path.isfile(token_file):\n        try:\n            os.remove(token_file)\n        except Exception as exc:  # pylint: disable=broad-except\n            _LOGGER.error(\n                \"Samsung TV - Error deleting token file %s: %s\", token_file, str(exc)\n            )\n\n\ndef _migrate_token(hass: HomeAssistant, entry: ConfigEntry, hostname: str) -> None:\n    \"\"\"Migrate token from old file to registry entry.\"\"\"\n    token_file = hass.config.path(STORAGE_DIR, token_file_name(hostname))\n    if not os.path.isfile(token_file):\n        token_file = (\n            os.path.dirname(os.path.realpath(__file__)) + f\"/token-{hostname}.txt\"\n        )\n        if not os.path.isfile(token_file):\n            return\n\n    try:\n        with open(token_file, \"r\", encoding=\"utf-8\") as os_token_file:\n            token = os_token_file.readline()\n    except Exception as exc:  # pylint: disable=broad-except\n        _LOGGER.error(\"Error reading token file %s: %s\", token_file, str(exc))\n        return\n\n    if not token:\n        _LOGGER.warning(\"No token found inside token file %s\", token_file)\n        return\n\n    hass.config_entries.async_update_entry(\n        entry, data={**entry.data, CONF_TOKEN: token}\n    )\n    _remove_token_file(hass, hostname, token_file)\n\n\n@callback\ndef _migrate_options_format(hass: HomeAssistant, entry: ConfigEntry) -> None:\n    \"\"\"Migrate options to new format.\"\"\"\n    opt_migrated = False\n    new_options = {}\n\n    for key, option in entry.options.items():\n        if key in [CONF_SYNC_TURN_OFF, CONF_SYNC_TURN_ON]:\n            if isinstance(option, str):\n                new_options[key] = option.split(\",\")\n                opt_migrated = True\n                continue\n        new_options[key] = option\n\n    # load the option lists in entry option\n    yaml_opt = hass.data.get(DOMAIN, {}).get(entry.entry_id, {}).get(DATA_CFG_YAML, {})\n    for key in [CONF_APP_LIST, CONF_CHANNEL_LIST, CONF_SOURCE_LIST]:\n        if key not in new_options:  # import will occurs only on first restart\n            if option := _load_option_list(yaml_opt.get(key, {})):\n                message = (\n                    f\"Configuration key '{key}' has been in imported in integration options,\"\n                    \" you can now remove from configuration.yaml\"\n                )\n                _notify_message(\n                    hass, f\"config-import-{key}\", \"SamsungTV Smart\", message\n                )\n                _LOGGER.warning(message)\n            new_options[key] = option\n            opt_migrated = True\n\n    if opt_migrated:\n        hass.config_entries.async_update_entry(entry, options=new_options)\n\n\n@callback\ndef _migrate_entry_unique_id(hass: HomeAssistant, entry: ConfigEntry) -> None:\n    \"\"\"Migrate unique_is to new format.\"\"\"\n    if CONF_ID in entry.data:\n        new_unique_id = entry.data[CONF_ID]\n    elif CONF_MAC in entry.data:\n        new_unique_id = entry.data[CONF_MAC]\n    else:\n        new_unique_id = entry.data[CONF_HOST]\n\n    if entry.unique_id == new_unique_id:\n        return\n\n    entries_list = hass.config_entries.async_entries(DOMAIN)\n    for other_entry in entries_list:\n        if other_entry.unique_id == new_unique_id:\n            _LOGGER.warning(\n                \"Found duplicated entries %s and %s that refer to the same device.\"\n                \" Please remove unused entry\",\n                entry.data[CONF_HOST],\n                other_entry.data[CONF_HOST],\n            )\n            return\n\n    _LOGGER.info(\n        \"Migrated entry unique id from %s to %s\", entry.unique_id, new_unique_id\n    )\n    hass.config_entries.async_update_entry(entry, unique_id=new_unique_id)\n\n\n@callback\ndef _migrate_smartthings_config(hass: HomeAssistant, entry: ConfigEntry) -> None:\n    \"\"\"Migrate smartthings entry usage configuration.\"\"\"\n    if CONF_USE_ST_INT_API_KEY not in entry.data:\n        return\n\n    new_data = entry.data.copy()\n    use_st = new_data.pop(CONF_USE_ST_INT_API_KEY)\n    if use_st:\n        if entries_list := hass.config_entries.async_entries(ST_DOMAIN, False, False):\n            new_data[CONF_ST_ENTRY_UNIQUE_ID] = entries_list[0].unique_id\n\n    hass.config_entries.async_update_entry(entry, data=new_data)\n\n\n@callback\ndef get_smartthings_entries(hass: HomeAssistant) -> dict[str, str] | None:\n    \"\"\"Get the smartthing integration configured entries.\"\"\"\n    entries_list = hass.config_entries.async_entries(ST_DOMAIN, False, False)\n    if not entries_list:\n        return None\n\n    return {\n        entry.unique_id: entry.title\n        for entry in entries_list\n        if CONF_TOKEN in entry.data\n    }\n\n\n@callback\ndef get_smartthings_api_key(hass: HomeAssistant, st_unique_id: str) -> str | None:\n    \"\"\"Get the smartthing integration configured API key.\"\"\"\n    entries_list = hass.config_entries.async_entries(ST_DOMAIN, False, False)\n    if not entries_list:\n        return None\n\n    for entry in entries_list:\n        if entry.unique_id == st_unique_id:\n            config_data = entry.data\n            if CONF_TOKEN not in config_data:\n                return None\n            return config_data[CONF_TOKEN].get(CONF_ACCESS_TOKEN)\n\n    return None\n\n\nasync def _register_logo_paths(hass: HomeAssistant) -> str | None:\n    \"\"\"Register paths for local logos.\"\"\"\n\n    static_logo_path = Path(__file__).parent / \"static\"\n    static_paths = [\n        StaticPathConfig(\n            STATIC_IMAGE_BASE_URL, str(static_logo_path), cache_headers=False\n        )\n    ]\n\n    local_logo_path = Path(hass.config.path(\"www\", f\"{DOMAIN}_logos\"))\n    url_logo_path = str(local_logo_path)\n    if not local_logo_path.exists():\n        try:\n            local_logo_path.mkdir(parents=True)\n        except Exception as exc:  # pylint: disable=broad-except\n            _LOGGER.warning(\n                \"Error registering custom logo folder %s: %s\", str(local_logo_path), exc\n            )\n            url_logo_path = None\n\n    if url_logo_path is not None:\n        static_paths.append(\n            StaticPathConfig(CUSTOM_IMAGE_BASE_URL, url_logo_path, cache_headers=False)\n        )\n\n    await hass.http.async_register_static_paths(static_paths)\n    return url_logo_path\n\n\nasync def get_device_info(hostname: str, session: ClientSession) -> dict:\n    \"\"\"Try retrieve device information\"\"\"\n    try:\n        async with async_timeout.timeout(2):\n            async with session.get(\n                tv_url(host=hostname), raise_for_status=True\n            ) as resp:\n                info = await resp.json()\n    except (asyncio.TimeoutError, ClientConnectionError):\n        _LOGGER.warning(\"Error getting HTTP device info for TV: %s\", hostname)\n        return {}\n\n    device = info.get(\"device\")\n    if not device:\n        _LOGGER.warning(\"Error getting HTTP device info for TV: %s\", hostname)\n        return {}\n\n    result = {\n        key: device[value] for key, value in DEVICE_INFO.items() if value in device\n    }\n\n    if ATTR_DEVICE_ID in result:\n        device_id = result[ATTR_DEVICE_ID]\n        if device_id.startswith(\"uuid:\"):\n            result[ATTR_DEVICE_ID] = device_id[len(\"uuid:\") :]\n\n    return result\n\n\nclass SamsungTVInfo:\n    \"\"\"Class to connect and collect TV information.\"\"\"\n\n    def __init__(self, hass, hostname, ws_name):\n        \"\"\"Initialize the object.\"\"\"\n        self._hass = hass\n        self._hostname = hostname\n        self._ws_name = ws_name\n        self._ws_port = 0\n        self._ws_token = None\n        self._ping_port = None\n\n    @property\n    def ws_port(self):\n        \"\"\"Return used WebSocket port.\"\"\"\n        return self._ws_port\n\n    @property\n    def ws_token(self):\n        \"\"\"Return WebSocket token.\"\"\"\n        return self._ws_token\n\n    @property\n    def ping_port(self):\n        \"\"\"Return the port used to ping the TV.\"\"\"\n        return self._ping_port\n\n    def _try_connect_ws(self):\n        \"\"\"Try to connect to device using web sockets on port 8001 and 8002\"\"\"\n\n        self._ping_port = SamsungTVWS.ping_probe(self._hostname)\n        if self._ping_port is None:\n            _LOGGER.error(\n                \"Connection to SamsungTV %s failed. Check that TV is on\", self._hostname\n            )\n            return RESULT_NOT_SUCCESSFUL\n\n        if self._ws_port and self._ws_token:\n            port_list = tuple([self._ws_port, 8001, 8002])\n        else:\n            port_list = (8001, 8002)\n\n        for index, port in enumerate(port_list):\n\n            timeout = 45  # We need this high timeout because waiting for TV auth popup\n            token = None\n            if len(port_list) > 2 and index == 0:\n                timeout = DEFAULT_TIMEOUT\n                token = self._ws_token\n\n            try:\n                _LOGGER.info(\n                    \"Try to configure SamsungTV %s using port %s%s\",\n                    self._hostname,\n                    str(port),\n                    \" with existing token\" if token else \"\",\n                )\n                with SamsungTVWS(\n                    name=f\"{WS_PREFIX} {self._ws_name}\",  # this is the name shown in the TV\n                    host=self._hostname,\n                    port=port,\n                    token=token,\n                    timeout=timeout,\n                ) as remote:\n                    remote.open()\n                    self._ws_token = remote.token\n                _LOGGER.info(\"Found working configuration using port %s\", str(port))\n                self._ws_port = port\n                return RESULT_SUCCESS\n            except (OSError, ConnectionFailure, WebSocketException) as err:\n                _LOGGER.info(\n                    \"Configuration failed using port %s, error: %s\", str(port), err\n                )\n\n        _LOGGER.error(\"Web socket connection to SamsungTV %s failed\", self._hostname)\n        return RESULT_NOT_SUCCESSFUL\n\n    @staticmethod\n    async def _try_connect_st(api_key, device_id, session: ClientSession):\n        \"\"\"Try to connect to ST device\"\"\"\n\n        try:\n            async with async_timeout.timeout(10):\n                _LOGGER.info(\"Try connection to SmartThings TV with id [%s]\", device_id)\n                with SmartThingsTV(\n                    api_key=api_key,\n                    device_id=device_id,\n                    session=session,\n                ) as st_tv:\n                    result = await st_tv.async_device_health()\n                if result:\n                    _LOGGER.info(\"Connection completed successfully.\")\n                    return RESULT_SUCCESS\n                _LOGGER.error(\"Connection to SmartThings TV not available.\")\n                return RESULT_ST_DEVICE_NOT_FOUND\n        except ClientResponseError as err:\n            _LOGGER.error(\"Failed connecting to SmartThings TV, error: %s\", err)\n            if err.status == 400:  # Bad request, means that token is valid\n                return RESULT_ST_DEVICE_NOT_FOUND\n        except Exception as err:  # pylint: disable=broad-except\n            _LOGGER.error(\"Failed connecting with SmartThings, error: %s\", err)\n\n        return RESULT_WRONG_APIKEY\n\n    @staticmethod\n    async def get_st_devices(api_key, session: ClientSession, st_device_label=\"\"):\n        \"\"\"Get list of available ST devices\"\"\"\n\n        try:\n            async with async_timeout.timeout(4):\n                devices = await SmartThingsTV.get_devices_list(\n                    api_key, session, st_device_label\n                )\n        except Exception as err:  # pylint: disable=broad-except\n            _LOGGER.error(\"Failed connecting with SmartThings, error: %s\", err)\n            return None\n\n        return devices\n\n    async def try_connect(\n        self,\n        session: ClientSession,\n        api_key=None,\n        st_device_id=None,\n        *,\n        ws_port=None,\n        ws_token=None,\n    ):\n        \"\"\"Try connect device\"\"\"\n        if session is None:\n            return RESULT_NOT_SUCCESSFUL\n\n        if ws_port and ws_token:\n            self._ws_port = ws_port\n            self._ws_token = ws_token\n\n        result = await self._hass.async_add_executor_job(self._try_connect_ws)\n        if result == RESULT_SUCCESS:\n            if api_key and st_device_id:\n                result = await self._try_connect_st(api_key, st_device_id, session)\n\n        return result\n\n\nasync def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:\n    \"\"\"Set up the Samsung TV integration.\"\"\"\n    if not is_valid_ha_version():\n        msg = (\n            \"This integration require at least HomeAssistant version\"\n            f\" {__min_ha_version__}, you are running version {__version__}.\"\n            \" Please upgrade HomeAssistant to continue use this integration.\"\n        )\n        _notify_message(hass, \"inv_ha_version\", \"SamsungTV Smart\", msg)\n        _LOGGER.warning(msg)\n        return True\n\n    if DOMAIN in config:\n        entries_list = hass.config_entries.async_entries(DOMAIN)\n        for entry_config in config[DOMAIN]:\n            # get ip address\n            ip_address = entry_config[CONF_HOST]\n\n            # check if already configured\n            valid_entries = [\n                entry.entry_id\n                for entry in entries_list\n                if entry.data[CONF_HOST] == ip_address\n            ]\n            if not valid_entries:\n                _LOGGER.warning(\n                    \"Found yaml configuration for not configured device %s.\"\n                    \" Please use UI to configure\",\n                    ip_address,\n                )\n                continue\n\n            data_yaml = {\n                key: value\n                for key, value in entry_config.items()\n                if key in SAMSMART_SCHEMA and value\n            }\n            if data_yaml:\n                if DOMAIN not in hass.data:\n                    hass.data[DOMAIN] = {}\n                hass.data[DOMAIN][valid_entries[0]] = {DATA_CFG_YAML: data_yaml}\n\n    # Register path for local logo\n    if local_logo_path := await _register_logo_paths(hass):\n        hass.data.setdefault(DOMAIN, {})[LOCAL_LOGO_PATH] = local_logo_path\n\n    return True\n\n\nasync def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:\n    \"\"\"Set up the Samsung TV platform.\"\"\"\n    if not is_valid_ha_version():\n        return False\n\n    # migrate unique id to a accepted format\n    _migrate_entry_unique_id(hass, entry)\n\n    # migrate smartthings entry usage configuration\n    _migrate_smartthings_config(hass, entry)\n\n    # migrate old token file to registry entry if required\n    if CONF_TOKEN not in entry.data:\n        await hass.async_add_executor_job(\n            _migrate_token, hass, entry, entry.data[CONF_HOST]\n        )\n\n    # migrate options to new format if required\n    _migrate_options_format(hass, entry)\n\n    # setup entry\n    if DOMAIN not in hass.data:\n        hass.data[DOMAIN] = {}\n\n    add_conf = None\n    config = entry.data.copy()\n    if entry.entry_id in hass.data[DOMAIN]:\n        add_conf = hass.data[DOMAIN][entry.entry_id].get(DATA_CFG_YAML, {})\n        for attr, value in add_conf.items():\n            if value:\n                config[attr] = value\n\n    # setup entry\n    hass.data[DOMAIN][entry.entry_id] = {\n        DATA_CFG: config,\n        DATA_OPTIONS: entry.options.copy(),\n    }\n    if add_conf:\n        hass.data[DOMAIN][entry.entry_id][DATA_CFG_YAML] = add_conf\n    entry.async_on_unload(entry.add_update_listener(_update_listener))\n\n    await hass.config_entries.async_forward_entry_setups(entry, SAMSMART_PLATFORM)\n\n    return True\n\n\nasync def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:\n    \"\"\"Unload a config entry.\"\"\"\n    if unload_ok := await hass.config_entries.async_unload_platforms(\n        entry, SAMSMART_PLATFORM\n    ):\n        hass.data[DOMAIN][entry.entry_id].pop(DATA_CFG)\n        hass.data[DOMAIN][entry.entry_id].pop(DATA_OPTIONS)\n        if not hass.data[DOMAIN][entry.entry_id]:\n            hass.data[DOMAIN].pop(entry.entry_id)\n\n    return unload_ok\n\n\nasync def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:\n    \"\"\"Remove a config entry.\"\"\"\n    await hass.async_add_executor_job(_remove_token_file, hass, entry.data[CONF_HOST])\n    if DOMAIN in hass.data:\n        hass.data[DOMAIN].pop(entry.entry_id, None)\n        if not hass.data[DOMAIN]:\n            hass.data.pop(DOMAIN)\n\n\nasync def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:\n    \"\"\"Update when config_entry options update.\"\"\"\n    hass.data[DOMAIN][entry.entry_id][DATA_OPTIONS] = entry.options.copy()\n    async_dispatcher_send(hass, SIGNAL_CONFIG_ENTITY)\n"
  },
  {
    "path": "custom_components/samsungtv_smart/api/__init__.py",
    "content": "\"\"\"SamsungTV Smart TV WS API library.\"\"\"\n"
  },
  {
    "path": "custom_components/samsungtv_smart/api/samsungcast.py",
    "content": "\"\"\"Smartthings TV integration cast tube.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport xml.etree.ElementTree as ET\n\nfrom casttube import YouTubeSession\nimport requests\n\n_LOGGER = logging.getLogger(__name__)\n\n\ndef _format_url(host: str, app: str) -> str:\n    \"\"\"Return URL used to retrieve screen id.\"\"\"\n    return f\"http://{host}:8080/ws/app/{app}\"\n\n\nclass CastTubeNotSupported(Exception):\n    \"\"\"Error during cast.\"\"\"\n\n\nclass SamsungCastTube:\n    \"\"\"Class to cast video to youtube TV app.\"\"\"\n\n    def __init__(self, host: str):\n        \"\"\"Initialize the class.\"\"\"\n        self._host = host\n        self._cast_api: YouTubeSession | None = None\n\n    @staticmethod\n    def _get_screen_id(host: str) -> str:\n        \"\"\"Retrieve screen id from the TV.\"\"\"\n        url = _format_url(host, \"YouTube\")\n        try:\n            response = requests.get(url, timeout=5)\n        except requests.ConnectionError as exc:\n            _LOGGER.warning(\n                \"Failed to retrieve YouTube screenID for host %s: %s\", host, exc\n            )\n            raise CastTubeNotSupported() from exc\n\n        screen_id = None\n        tree = ET.fromstring(response.content.decode(\"utf8\"))\n        for elem in tree.iter():\n            if elem.tag.endswith(\"screenId\"):\n                _LOGGER.debug(\"YouTube ScreenID: %s\", screen_id)\n                screen_id = elem.text\n\n        if screen_id is None:\n            _LOGGER.warning(\"Failed to retrieve YouTube screenID for host %s\", host)\n            raise CastTubeNotSupported()\n\n        return screen_id\n\n    def _get_api(self) -> YouTubeSession:\n        \"\"\"Get the API to cast video.\"\"\"\n        if not self._cast_api:\n            screen_id = self._get_screen_id(self._host)\n            self._cast_api = YouTubeSession(screen_id)\n        return self._cast_api\n\n    def play_video(self, video_id: str) -> None:\n        \"\"\"Play video_id immediatly.\"\"\"\n        self._get_api().play_video(video_id)\n\n    def play_next(self, video_id: str) -> None:\n        \"\"\"Play video_id after the currently playing video..\"\"\"\n        self._get_api().play_next(video_id)\n\n    def add_to_queue(self, video_id: str) -> None:\n        \"\"\"Add a video to the video queue.\"\"\"\n        self._get_api().add_to_queue(video_id)\n\n    def clear_queue(self) -> None:\n        \"\"\"Clear the video queue.\"\"\"\n        self._get_api().clear_playlist()\n"
  },
  {
    "path": "custom_components/samsungtv_smart/api/samsungws.py",
    "content": "\"\"\"\nSamsungTVWS - Samsung Smart TV WS API wrapper\n\nCopyright (C) 2019 Xchwarze\nCopyright (C) 2020 Ollo69\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor,\n    Boston, MA  02110-1335  USA\n\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nfrom datetime import datetime, timezone\nfrom enum import Enum\nimport json\nimport logging\nimport socket\nimport ssl\nimport subprocess\nimport sys\nfrom threading import Lock, Thread\nimport time\nfrom typing import Any\nfrom urllib.parse import urlencode, urljoin\nimport uuid\n\nimport aiohttp\nimport requests\nimport websocket\n\nfrom .shortcuts import SamsungTVShortcuts\n\nDEFAULT_POWER_ON_DELAY = 120\nMIN_APP_SCAN_INTERVAL = 9\nMAX_APP_VALIDITY_SEC = 60\nMAX_WS_PING_INTERVAL = 10\nPING_TIMEOUT = 3\nTYPE_DEEP_LINK = \"DEEP_LINK\"\nTYPE_NATIVE_LAUNCH = \"NATIVE_LAUNCH\"\n\n_WS_ENDPOINT_REMOTE_CONTROL = \"/api/v2/channels/samsung.remote.control\"\n_WS_ENDPOINT_APP_CONTROL = \"/api/v2\"\n_WS_ENDPOINT_ART = \"/api/v2/channels/com.samsung.art-app\"\n_WS_LOG_NAME = \"websocket\"\n\n_LOG_PING_PONG = False\n_LOGGING = logging.getLogger(__name__)\n\n\ndef _set_ws_logger_level(level: int = logging.CRITICAL) -> None:\n    \"\"\"Set the websocket library logging level.\"\"\"\n    ws_logger = logging.getLogger(_WS_LOG_NAME)\n    if ws_logger.level < level:\n        ws_logger.setLevel(level)\n\n\ndef _format_rest_url(host: str, append: str = \"\") -> str:\n    \"\"\"Return URL used for rest commands.\"\"\"\n    return f\"http://{host}:8001/api/v2/{append}\"\n\n\ndef gen_uuid() -> str:\n    \"\"\"Generate new uuid.\"\"\"\n    return str(uuid.uuid4())\n\n\ndef aware_utcnow() -> datetime:\n    \"\"\"Return current UTC time with timezone info.\"\"\"\n    return datetime.now(timezone.utc)\n\n\ndef kill_subprocess(\n    process: subprocess.Popen[Any],\n) -> None:\n    \"\"\"Force kill a subprocess and wait for it to exit.\"\"\"\n    process.kill()\n    process.communicate()\n    process.wait()\n\n    del process\n\n\ndef _process_api_response(response, *, raise_error=True):\n    \"\"\"Process response received by TV.\"\"\"\n    try:\n        return json.loads(response)\n    except json.JSONDecodeError as exc:\n        _LOGGING.debug(\"Failed to parse response from TV. response text: %s\", response)\n        if raise_error:\n            raise ResponseError(\n                \"Failed to parse response from TV. Maybe feature not supported on this model\"\n            ) from exc\n    return response\n\n\ndef _log_ping_pong(msg, *args):\n    \"\"\"Log ping pong message if enabled.\"\"\"\n    if not _LOG_PING_PONG:\n        return\n    _LOGGING.debug(msg=msg, args=args)\n\n\nclass Ping:\n    \"\"\"Class for handling Ping to a specific host.\"\"\"\n\n    def __init__(self, host):\n        \"\"\"Initialize the object.\"\"\"\n        self._ip_address = host\n        if sys.platform == \"win32\":\n            self._ping_cmd = [\"ping\", \"-n\", \"1\", \"-w\", \"2000\", host]\n        else:\n            self._ping_cmd = [\"ping\", \"-n\", \"-q\", \"-c1\", \"-W2\", host]\n\n    def ping(self, port=0):\n        \"\"\"Check if IP is available using ICMP or trying open a specific port.\"\"\"\n        if port > 0:\n            return self._ping_socket(port)\n        return self._ping()\n\n    def _ping(self):\n        \"\"\"Send ICMP echo request and return True if success.\"\"\"\n        with subprocess.Popen(\n            self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL\n        ) as pinger:\n            try:\n                pinger.communicate(timeout=1 + PING_TIMEOUT)\n                return pinger.returncode == 0\n            except subprocess.TimeoutExpired:\n                kill_subprocess(pinger)\n                return False\n            except subprocess.CalledProcessError:\n                return False\n\n    def _ping_socket(self, port):\n        \"\"\"Check if port is available and return True if success.\"\"\"\n        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:\n            sock.settimeout(PING_TIMEOUT - 1)\n            return sock.connect_ex((self._ip_address, port)) == 0\n\n\nclass ConnectionFailure(Exception):\n    \"\"\"Error during connection.\"\"\"\n\n\nclass ResponseError(Exception):\n    \"\"\"Error in response.\"\"\"\n\n\nclass HttpApiError(Exception):\n    \"\"\"Error using HTTP API.\"\"\"\n\n\nclass App:\n    \"\"\"Define a TV Application.\"\"\"\n\n    def __init__(self, app_id, app_name, app_type):\n        self.app_id = app_id\n        self.app_name = app_name\n        self.app_type = app_type\n\n\nclass ArtModeStatus(Enum):\n    \"\"\"Define possible ArtMode status.\"\"\"\n\n    Unsupported = 0\n    Unavailable = 1\n    Off = 2\n    On = 3\n\n\nclass SamsungTVAsyncRest:\n    \"\"\"Class that implement rest request in async.\"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        session: aiohttp.ClientSession,\n        timeout=None,\n    ) -> None:\n        \"\"\"Initialize the class.\"\"\"\n        self._host = host\n        self._session = session\n        self._timeout = None if timeout == 0 else timeout\n\n    async def _rest_request(self, target: str, method: str = \"GET\") -> dict[str, Any]:\n        \"\"\"Perform async rest request.\"\"\"\n        url = _format_rest_url(self._host, target)\n        try:\n            if method == \"POST\":\n                req = self._session.post(url, timeout=self._timeout, verify_ssl=False)\n            elif method == \"PUT\":\n                req = self._session.put(url, timeout=self._timeout, verify_ssl=False)\n            elif method == \"DELETE\":\n                req = self._session.delete(url, timeout=self._timeout, verify_ssl=False)\n            else:\n                req = self._session.get(url, timeout=self._timeout, verify_ssl=False)\n            async with req as resp:\n                return _process_api_response(await resp.text())\n        except aiohttp.ClientConnectionError as ex:\n            raise HttpApiError(\n                \"TV unreachable or feature not supported on this model.\"\n            ) from ex\n\n    async def async_rest_device_info(self) -> dict[str, Any]:\n        \"\"\"Get device info using rest api call.\"\"\"\n        _LOGGING.debug(\"Get device info via rest api\")\n        return await self._rest_request(\"\")\n\n    async def async_rest_app_status(self, app_id: str) -> dict[str, Any]:\n        \"\"\"Get app status using rest api call.\"\"\"\n        _LOGGING.debug(\"Get app %s status via rest api\", app_id)\n        return await self._rest_request(\"applications/\" + app_id)\n\n    async def async_rest_app_run(self, app_id: str) -> dict[str, Any]:\n        \"\"\"Run an app using rest api call.\"\"\"\n        _LOGGING.debug(\"Run app %s via rest api\", app_id)\n        return await self._rest_request(\"applications/\" + app_id, \"POST\")\n\n    async def async_rest_app_close(self, app_id: str) -> dict[str, Any]:\n        \"\"\"Close an app using rest api call.\"\"\"\n        _LOGGING.debug(\"Close app %s via rest api\", app_id)\n        return await self._rest_request(\"applications/\" + app_id, \"DELETE\")\n\n    async def async_rest_app_install(self, app_id: str) -> dict[str, Any]:\n        \"\"\"Install a new app using rest api call.\"\"\"\n        _LOGGING.debug(\"Install app %s via rest api\", app_id)\n        return await self._rest_request(\"applications/\" + app_id, \"PUT\")\n\n\nclass SamsungTVWS:\n    \"\"\"Class to manage websocket communication with tizen TV.\"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        *,\n        token: str | None = None,\n        token_file: str | None = None,\n        port: int | None = 8001,\n        timeout: int | None = None,\n        key_press_delay: float | None = 1.0,\n        name: str | None = \"SamsungTvRemote\",\n        app_list: dict | None = None,\n        ping_port: int | None = 0,\n    ):\n        \"\"\"Initialize SamsungTVWS object.\"\"\"\n        self.host = host\n        self.token = token\n        self.token_file = token_file\n        self.port = port or 8001\n        self.timeout = None if timeout == 0 else timeout\n        self.key_press_delay = 1.0 if key_press_delay is None else key_press_delay\n        self.name = name or \"SamsungTvRemote\"\n        self._app_list = dict(app_list) if app_list else None\n        self._ping_port = ping_port or 0\n\n        self.connection = None\n        self._artmode_status = ArtModeStatus.Unsupported\n        self._power_on_requested = False\n        self._power_on_requested_time = datetime.min\n        self._power_on_delay = DEFAULT_POWER_ON_DELAY\n        self._power_on_artmode = False\n\n        self._installed_app = {}\n        self._running_apps: dict[str, datetime] = {}\n        self._running_app: str | None = None\n        self._running_app_changed: bool | None = None\n        self._last_running_scan = aware_utcnow()\n        self._app_type = {}\n        self._sync_lock = Lock()\n        self._last_app_scan = datetime.min\n\n        self._ping_thread = None\n        self._ping_thread_run = False\n\n        self._ws_remote = None\n        self._client_remote = None\n        self._last_ping = datetime.min\n        self._is_connected = False\n\n        self._ws_control = None\n        self._client_control = None\n        self._last_control_ping = datetime.min\n        self._is_control_connected = False\n\n        self._ws_art = None\n        self._client_art = None\n        self._last_art_ping = datetime.min\n        self._client_art_supported = 2\n\n        self._ping = Ping(self.host)\n        self._status_callback = None\n        self._new_token_callback = None\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_value, exc_traceback):\n        self.close()\n\n    @staticmethod\n    def ping_probe(host):\n        \"\"\"Try to ping device and return usable port.\"\"\"\n        ping = Ping(host)\n        for port in (9197, 0):\n            try:\n                if ping.ping(port):\n                    return port\n            except Exception:  # pylint: disable=broad-except\n                _LOGGING.debug(\"Failed to ping device using port %s\", port)\n\n        return None\n\n    @staticmethod\n    def _serialize_string(string):\n        if isinstance(string, str):\n            string = str.encode(string)\n        return base64.b64encode(string).decode(\"utf-8\")\n\n    def _is_ssl_connection(self):\n        return self.port == 8002\n\n    def _format_websocket_url(self, path, is_ssl=False, use_token=True):\n        scheme = \"wss\" if is_ssl else \"ws\"\n\n        base_uri = f\"{scheme}://{self.host}:{self.port}\"\n        ws_uri = urljoin(base_uri, path)\n        query = {\"name\": self._serialize_string(self.name)}\n        if is_ssl and use_token:\n            if token := self._get_token():\n                query[\"token\"] = token\n        ws_query = urlencode(query)\n        return f\"{ws_uri}?{ws_query}\"\n\n    def set_ping_port(self, port: int):\n        \"\"\"Set a new ping port.\"\"\"\n        self._ping_port = port\n\n    def update_app_list(self, app_list: dict | None):\n        \"\"\"Update application list.\"\"\"\n        self._app_list = dict(app_list) if app_list else None\n\n    def register_new_token_callback(self, func):\n        \"\"\"Register a callback function.\"\"\"\n        self._new_token_callback = func\n\n    def register_status_callback(self, func):\n        \"\"\"Register callback function used on status change.\"\"\"\n        self._status_callback = func\n\n    def unregister_status_callback(self):\n        \"\"\"Unregister callback function used on status change.\"\"\"\n        self._status_callback = None\n\n    def _get_token(self):\n        \"\"\"Get current token.\"\"\"\n        if self.token_file is not None:\n            try:\n                with open(self.token_file, \"r\", encoding=\"utf-8\") as token_file:\n                    return token_file.readline()\n            except Exception as exc:  # pylint: disable=broad-except\n                _LOGGING.error(\"Failed to read TV token file: %s\", str(exc))\n                return \"\"\n        return self.token\n\n    def _set_token(self, token):\n        \"\"\"Save new token.\"\"\"\n        _LOGGING.debug(\"New token %s\", token)\n        if self.token_file is not None:\n            _LOGGING.debug(\"Save new token to file %s\", self.token_file)\n            with open(self.token_file, \"w\", encoding=\"utf-8\") as token_file:\n                token_file.write(token)\n            return\n\n        if self.token is not None and self.token == token:\n            return\n        self.token = token\n        if self._new_token_callback is not None:\n            self._new_token_callback()\n\n    def _ws_send(\n        self,\n        command,\n        key_press_delay=None,\n        *,\n        use_control=False,\n        ws_socket=None,\n        raise_on_closed=False,\n    ):\n        \"\"\"Send a command using the appropriate websocket.\"\"\"\n        using_remote = False\n        if not use_control:\n            if self._ws_remote:\n                connection = self._ws_remote\n                using_remote = True\n            else:\n                connection = self.open()\n        elif ws_socket:\n            connection = ws_socket\n        else:\n            self._start_client(start_all=True)\n            return False\n\n        payload = json.dumps(command)\n        try:\n            connection.send(payload)\n        except websocket.WebSocketConnectionClosedException:\n            if raise_on_closed:\n                raise\n            _LOGGING.warning(\"_ws_send: connection is closed, send command failed\")\n            if using_remote or use_control:\n                _LOGGING.info(\"_ws_send: try to restart communication threads\")\n                self._start_client(start_all=use_control)\n            return False\n        except websocket.WebSocketTimeoutException:\n            _LOGGING.warning(\"_ws_send: timeout error sending command %s\", payload)\n            return False\n\n        if using_remote:\n            # we consider a message sent valid as a ping\n            self._last_ping = aware_utcnow()\n\n        if key_press_delay is None:\n            if self.key_press_delay > 0:\n                time.sleep(self.key_press_delay)\n        elif key_press_delay > 0:\n            time.sleep(key_press_delay)\n\n        return True\n\n    def _rest_request(self, target, method=\"GET\"):\n        \"\"\"Send a rest command using http protocol.\"\"\"\n        url = _format_rest_url(self.host, target)\n        try:\n            if method == \"POST\":\n                response = requests.post(url, timeout=self.timeout)\n            elif method == \"PUT\":\n                response = requests.put(url, timeout=self.timeout)\n            elif method == \"DELETE\":\n                response = requests.delete(url, timeout=self.timeout)\n            else:\n                response = requests.get(url, timeout=self.timeout)\n        except requests.ConnectionError as exc:\n            raise HttpApiError(\n                \"TV unreachable or feature not supported on this model.\"\n            ) from exc\n        return _process_api_response(response.text, raise_error=False)\n\n    def _check_conn_id(self, resp_data):\n        \"\"\"Check if returned connection id from WS server is valid for this TV.\"\"\"\n        if not resp_data:\n            return False\n\n        msg_id = resp_data.get(\"id\")\n        if not msg_id:\n            return False\n\n        clients_info = resp_data.get(\"clients\")\n        for client in clients_info:\n            device_name = client.get(\"deviceName\")\n            if device_name:\n                if device_name == self._serialize_string(self.name):\n                    conn_id = client.get(\"id\", \"\")\n                    if conn_id == msg_id:\n                        return True\n        return False\n\n    @staticmethod\n    def _run_forever(\n        ws_app: websocket.WebSocketApp, *, sslopt: dict = None, ping_interval: int = 0\n    ) -> None:\n        \"\"\"Call method run_forever changing library log level before.\"\"\"\n        _set_ws_logger_level()\n        ws_app.run_forever(sslopt=sslopt, ping_interval=ping_interval)\n\n    def _client_remote_thread(self):\n        \"\"\"Start the main client WS thread that connect to the remote TV.\"\"\"\n        if self._ws_remote:\n            return\n\n        is_ssl = self._is_ssl_connection()\n        url = self._format_websocket_url(_WS_ENDPOINT_REMOTE_CONTROL, is_ssl=is_ssl)\n        sslopt = {\"cert_reqs\": ssl.CERT_NONE} if is_ssl else {}\n\n        websocket.setdefaulttimeout(self.timeout)\n        self._ws_remote = websocket.WebSocketApp(\n            url,\n            on_message=self._on_message_remote,\n            on_ping=self._on_ping_remote,\n        )\n        _LOGGING.debug(\"Thread SamsungRemote started\")\n        # we set ping interval (1 hour) only to enable multi-threading mode\n        # on socket. TV do not answer to ping but send ping to client\n        self._run_forever(self._ws_remote, sslopt=sslopt, ping_interval=3600)\n        self._is_connected = False\n        if self._status_callback is not None:\n            self._status_callback()\n        if self._ws_art:\n            self._ws_art.close()\n        if self._ws_control:\n            self._ws_control.close()\n        self._ws_remote.close()\n        self._ws_remote = None\n        _LOGGING.debug(\"Thread SamsungRemote terminated\")\n\n    def _on_ping_remote(self, _, payload):\n        \"\"\"Manage ping message received by remote WS connection.\"\"\"\n        _log_ping_pong(\"Received WS remote ping %s, sending pong\", payload)\n        self._last_ping = aware_utcnow()\n        if self._ws_remote.sock:\n            try:\n                self._ws_remote.sock.pong(payload)\n            except Exception as ex:  # pylint: disable=broad-except\n                _LOGGING.warning(\"WS remote send_pong failed, %s\", ex)\n\n    def _on_message_remote(self, _, message):\n        \"\"\"Manage messages received by remote WS connection.\"\"\"\n        response = _process_api_response(message)\n        _LOGGING.debug(response)\n        event = response.get(\"event\")\n        if not event:\n            return\n\n        # we consider a message valid as a ping\n        self._last_ping = aware_utcnow()\n\n        if event == \"ms.channel.connect\":\n            conn_data = response.get(\"data\")\n            if not self._check_conn_id(conn_data):\n                return\n            _LOGGING.debug(\"Message remote: received connect\")\n            token = conn_data.get(\"token\")\n            if token:\n                self._set_token(token)\n            self._is_connected = True\n            self._request_apps_list()\n            self._start_client(start_all=True)\n            if self._status_callback is not None:\n                self._status_callback()\n        elif event == \"ed.installedApp.get\":\n            _LOGGING.debug(\"Message remote: received installedApp\")\n            self._handle_installed_app(response)\n        elif event == \"ed.edenTV.update\":\n            _LOGGING.debug(\"Message remote: received edenTV\")\n            self._get_running_app(force_scan=True)\n\n    def _request_apps_list(self):\n        \"\"\"Request to the TV the list of installed apps.\"\"\"\n        _LOGGING.debug(\"Request app list\")\n        self._ws_send(\n            {\n                \"method\": \"ms.channel.emit\",\n                \"params\": {\"event\": \"ed.installedApp.get\", \"to\": \"host\"},\n            },\n            key_press_delay=0,\n        )\n\n    def _handle_installed_app(self, response):\n        \"\"\"Manage the list of installed apps received from the TV.\"\"\"\n        list_app = response.get(\"data\", {}).get(\"data\")\n        installed_app = {}\n        for app_info in list_app:\n            app_id = app_info[\"appId\"]\n            _LOGGING.debug(\"Found app: %s\", app_id)\n            app = App(app_id, app_info[\"name\"], app_info[\"app_type\"])\n            installed_app[app_id] = app\n        self._installed_app = installed_app\n\n    def _client_control_thread(self):\n        \"\"\"Start the client control WS thread used to manage running apps.\"\"\"\n        if self._ws_control:\n            return\n\n        is_ssl = self._is_ssl_connection()\n        url = self._format_websocket_url(\n            _WS_ENDPOINT_APP_CONTROL, is_ssl=is_ssl, use_token=False\n        )\n        sslopt = {\"cert_reqs\": ssl.CERT_NONE} if is_ssl else {}\n\n        websocket.setdefaulttimeout(self.timeout)\n        self._ws_control = websocket.WebSocketApp(\n            url,\n            on_message=self._on_message_control,\n            on_ping=self._on_ping_control,\n        )\n        _LOGGING.debug(\"Thread SamsungControl started\")\n        # we set ping interval (1 hour) only to enable multi-threading mode\n        # on socket. TV do not answer to ping but send ping to client\n        self._run_forever(self._ws_control, sslopt=sslopt, ping_interval=3600)\n        self._is_control_connected = False\n        self._ws_control.close()\n        self._ws_control = None\n        self._running_app_changed = None\n        _LOGGING.debug(\"Thread SamsungControl terminated\")\n\n    def _on_ping_control(self, _, payload):\n        \"\"\"Manage ping message received by control WS channel.\"\"\"\n        _log_ping_pong(\"Received WS control ping %s, sending pong\", payload)\n        self._last_control_ping = aware_utcnow()\n        if self._ws_control.sock:\n            try:\n                self._ws_control.sock.pong(payload)\n            except Exception as ex:  # pylint: disable=broad-except\n                _LOGGING.warning(\"WS control send_pong failed, %s\", ex)\n\n    def _on_message_control(self, _, message):\n        \"\"\"Manage messages received by control WS channel.\"\"\"\n        response = _process_api_response(message)\n        _LOGGING.debug(response)\n        result = response.get(\"result\")\n        if result:\n            self._set_running_app(response)\n            return\n        error = response.get(\"error\")\n        if error:\n            self._manage_control_err(response)\n            return\n        event = response.get(\"event\")\n        if not event:\n            return\n        if event == \"ms.channel.connect\":\n            conn_data = response.get(\"data\")\n            if not self._check_conn_id(conn_data):\n                return\n            _LOGGING.debug(\"Message control: received connect\")\n            self._is_control_connected = True\n            self._get_running_app()\n        elif event == \"ed.installedApp.get\":\n            _LOGGING.debug(\"Message control: received installedApp\")\n            self._handle_installed_app(response)\n\n    def _set_running_app(self, response):\n        \"\"\"Set the current running app based on received message.\"\"\"\n        if not (app_id := response.get(\"id\")):\n            return\n        if (result := response.get(\"result\")) is None:\n            return\n        if isinstance(result, bool):\n            is_running = result\n        elif (is_running := result.get(\"visible\")) is None:\n            return\n\n        call_time = aware_utcnow()\n        self._last_running_scan = call_time\n        self._running_apps[app_id] = call_time\n        if self._running_app:\n            if is_running and app_id != self._running_app:\n                _LOGGING.debug(\"app running: %s\", app_id)\n                self._running_app = app_id\n                self._running_app_changed = True\n            elif not is_running and app_id == self._running_app:\n                _LOGGING.debug(\"app stopped: %s\", app_id)\n                self._running_app = None\n                self._running_app_changed = True\n        elif is_running:\n            _LOGGING.debug(\"app running: %s\", app_id)\n            self._running_app = app_id\n            self._running_app_changed = True\n\n        if self._running_app_changed is None:\n            self._running_app_changed = True\n\n    def _manage_control_err(self, response):\n        \"\"\"Manage errors from control WS channel.\"\"\"\n        app_id = response.get(\"id\")\n        if not app_id:\n            return\n        error_code = response.get(\"error\", {}).get(\"code\", 0)\n        if error_code == 404:  # Not found error\n            if self._installed_app:\n                if app_id not in self._installed_app:\n                    _LOGGING.error(\"App ID %s not found\", app_id)\n                return\n            # app_type = self._app_type.get(app_id)\n            # if app_type is None:\n            #     _LOGGING.info(\n            #         \"App ID %s with type DEEP_LINK not found, set as NATIVE_LAUNCH\",\n            #         app_id,\n            #     )\n            #     self._app_type[app_id] = 4\n\n    def _get_app_status(self, app_id, app_type):\n        \"\"\"Send a message to control WS channel to get the app status.\"\"\"\n        _LOGGING.debug(\"Get app status: AppID: %s, AppType: %s\", app_id, app_type)\n\n        if not (self._ws_control and self._is_control_connected):\n            return\n\n        if app_type == 4:  # app type 4 always return not found error\n            return\n\n        method = \"ms.application.get\"\n        try:\n            self._ws_send(\n                {\n                    \"id\": app_id,\n                    \"method\": method,\n                    \"params\": {\"id\": app_id},\n                },\n                key_press_delay=0,\n                use_control=True,\n                ws_socket=self._ws_control,\n                raise_on_closed=True,\n            )\n        except websocket.WebSocketConnectionClosedException:\n            _LOGGING.debug(\"Get app status aborted: connection closed\")\n\n    def _client_art_thread(self):\n        \"\"\"Start the client art WS thread used to manage art mode status.\"\"\"\n        if self._ws_art:\n            return\n\n        is_ssl = self._is_ssl_connection()\n        url = self._format_websocket_url(\n            _WS_ENDPOINT_ART, is_ssl=is_ssl, use_token=False\n        )\n        sslopt = {\"cert_reqs\": ssl.CERT_NONE} if is_ssl else {}\n\n        websocket.setdefaulttimeout(self.timeout)\n        self._ws_art = websocket.WebSocketApp(\n            url,\n            on_message=self._on_message_art,\n            on_ping=self._on_ping_art,\n        )\n        _LOGGING.debug(\"Thread SamsungArt started\")\n        # we set ping interval (1 hour) only to enable multi-threading mode\n        # on socket. TV do not answer to ping but send ping to client\n        self._run_forever(self._ws_art, sslopt=sslopt, ping_interval=3600)\n        self._ws_art.close()\n        self._ws_art = None\n        _LOGGING.debug(\"Thread SamsungArt terminated\")\n\n    def _on_ping_art(self, _, payload):\n        \"\"\"Manage ping message received by art WS channel.\"\"\"\n        _log_ping_pong(\"Received WS art ping %s, sending pong\", payload)\n        self._last_art_ping = aware_utcnow()\n        if self._ws_art.sock:\n            try:\n                self._ws_art.sock.pong(payload)\n            except Exception as ex:  # pylint: disable=broad-except\n                _LOGGING.warning(\"WS art send_pong failed: %s\", ex)\n\n    def _on_message_art(self, _, message):\n        \"\"\"Manage messages received by art WS channel.\"\"\"\n        response = _process_api_response(message)\n        _LOGGING.debug(response)\n        event = response.get(\"event\")\n        if not event:\n            return\n\n        # we consider a message valid as a ping\n        self._last_art_ping = aware_utcnow()\n\n        if event == \"ms.channel.connect\":\n            conn_data = response.get(\"data\")\n            if not self._check_conn_id(conn_data):\n                return\n            _LOGGING.debug(\"Message art: received connect\")\n            self._client_art_supported = 1\n        elif event == \"ms.channel.ready\":\n            _LOGGING.debug(\"Message art: channel ready\")\n            self._get_artmode_status()\n        elif event == \"d2d_service_message\":\n            _LOGGING.debug(\"Message art: d2d message\")\n            self._handle_artmode_status(response)\n\n    def _get_artmode_status(self):\n        \"\"\"Detect current art mode based on received message.\"\"\"\n        _LOGGING.debug(\"Sending get_art_status\")\n        msg_data = {\n            \"request\": \"get_artmode_status\",\n            \"id\": gen_uuid(),\n        }\n        self._ws_send(\n            {\n                \"method\": \"ms.channel.emit\",\n                \"params\": {\n                    \"data\": json.dumps(msg_data),\n                    \"to\": \"host\",\n                    \"event\": \"art_app_request\",\n                },\n            },\n            key_press_delay=0,\n            use_control=True,\n            ws_socket=self._ws_art,\n        )\n\n    def _handle_artmode_status(self, response):\n        \"\"\"Handle received art mode status.\"\"\"\n        data_str = response.get(\"data\")\n        if not data_str:\n            return\n        data = _process_api_response(data_str)\n        event = data.get(\"event\", \"\")\n        if event == \"art_mode_changed\":\n            status = data.get(\"status\", \"\")\n            if status == \"on\":\n                artmode_status = ArtModeStatus.On\n            else:\n                artmode_status = ArtModeStatus.Off\n        elif event == \"artmode_status\":\n            value = data.get(\"value\", \"\")\n            if value == \"on\":\n                artmode_status = ArtModeStatus.On\n            else:\n                artmode_status = ArtModeStatus.Off\n        elif event == \"go_to_standby\":\n            artmode_status = ArtModeStatus.Unavailable\n        elif event == \"wakeup\":\n            self._get_artmode_status()\n            return\n        else:\n            # Unknown message\n            return\n\n        if self._power_on_requested and artmode_status != ArtModeStatus.Unavailable:\n            if artmode_status == ArtModeStatus.On and not self._power_on_artmode:\n                self.send_key(\"KEY_POWER\", key_press_delay=0)\n            elif artmode_status == ArtModeStatus.Off and self._power_on_artmode:\n                self.send_key(\"KEY_POWER\", key_press_delay=0)\n            self._power_on_requested = False\n\n        self._artmode_status = artmode_status\n\n    @property\n    def is_connected(self):\n        \"\"\"Return if WS connection is open.\"\"\"\n        return self._is_connected\n\n    @property\n    def artmode_status(self):\n        \"\"\"Return current art mode status.\"\"\"\n        return self._artmode_status\n\n    @property\n    def installed_app(self):\n        \"\"\"Return a list of installed apps.\"\"\"\n        return self._installed_app\n\n    @property\n    def running_app(self):\n        \"\"\"Return current running app.\"\"\"\n        return self._running_app\n\n    def is_app_running(self, app_id: str) -> bool | None:\n        \"\"\"Return if app_id is running app.\"\"\"\n        if app_id == self._running_app:\n            return True\n        if (last_seen := self._running_apps.get(app_id)) is None:\n            return None\n        app_age = (self._last_running_scan - last_seen).total_seconds()\n        if app_age >= MAX_APP_VALIDITY_SEC:\n            self._running_apps.pop(app_id)\n            return None\n        return False\n\n    def _ping_thread_method(self):\n        \"\"\"Start the ping thread that check the TV status.\"\"\"\n        ping = Ping(self.host)\n        while self._ping_thread_run:\n            if ping.ping(self._ping_port):\n                if not self._is_connected:\n                    self._start_client()\n                else:\n                    self._check_remote()\n            else:\n                if self._is_connected:\n                    self.stop_client()\n            time.sleep(1.0)\n\n    def _check_remote(self):\n        \"\"\"Check current remote thread status.\"\"\"\n        call_time = aware_utcnow()\n        if self._ws_remote:\n            difference = (call_time - self._last_ping).total_seconds()\n            if difference >= MAX_WS_PING_INTERVAL:\n                self.stop_client()\n                if self._artmode_status != ArtModeStatus.Unsupported:\n                    self._artmode_status = ArtModeStatus.Unavailable\n            else:\n                self._check_art_mode()\n                self._get_running_app()\n                self._notify_app_change()\n\n        if self._power_on_requested:\n            difference = (call_time - self._power_on_requested_time).total_seconds()\n            if difference > self._power_on_delay:\n                self._power_on_requested = False\n\n    def _check_art_mode(self):\n        \"\"\"Check current art mode and start related control thread if required.\"\"\"\n        if self._artmode_status == ArtModeStatus.Unsupported:\n            return\n        if self._ws_art:\n            difference = (aware_utcnow() - self._last_art_ping).total_seconds()\n            if difference >= MAX_WS_PING_INTERVAL:\n                self._artmode_status = ArtModeStatus.Unavailable\n                self._ws_art.close()\n        elif self._ws_remote:\n            self._start_client(start_all=True)\n\n    def _notify_app_change(self):\n        \"\"\"Notify that running app is changed.\"\"\"\n        if not self._running_app_changed:\n            return\n        if not self._status_callback:\n            self._running_app_changed = False\n            return\n        last_change = (aware_utcnow() - self._last_running_scan).total_seconds()\n        if last_change >= 2:  # delay 2 seconds before calling\n            self._running_app_changed = False\n            self._status_callback()\n\n    def _get_running_app(self, *, force_scan=False):\n        \"\"\"Query current running app using control channel.\"\"\"\n        if not (self._ws_control and self._is_control_connected):\n            return\n\n        scan_interval = 1 if force_scan else MIN_APP_SCAN_INTERVAL\n        with self._sync_lock:\n            call_time = aware_utcnow()\n            difference = (call_time - self._last_app_scan).total_seconds()\n            if difference < scan_interval:\n                return\n            self._last_app_scan = call_time\n\n        if self._app_list is not None:\n            app_to_check = {}\n            for app_name, app_id in self._app_list.items():\n                app = None\n                if self._installed_app:\n                    app = self._installed_app.get(app_id)\n                else:\n                    app_type = self._app_type.get(app_id, 2)\n                    if app_type <= 4:\n                        app = App(app_id, app_name, app_type)\n                if app:\n                    app_to_check[app_id] = app\n        else:\n            app_to_check = self._installed_app\n\n        for app in app_to_check.values():\n            self._get_app_status(app.app_id, app.app_type)\n\n    def set_power_on_request(self, set_art_mode=False, power_on_delay=0):\n        \"\"\"Set a power on request status and save the time of the rquest.\"\"\"\n        self._power_on_requested = True\n        self._power_on_requested_time = aware_utcnow()\n        self._power_on_artmode = set_art_mode\n        self._power_on_delay = max(power_on_delay, 0) or DEFAULT_POWER_ON_DELAY\n\n    def set_power_off_request(self):\n        \"\"\"Remove a previous power on request.\"\"\"\n        self._power_on_requested = False\n\n    def start_poll(self):\n        \"\"\"Start polling the TV for status.\"\"\"\n        if self._ping_thread is None or not self._ping_thread.is_alive():\n            self._ping_thread = Thread(target=self._ping_thread_method)\n            self._ping_thread.name = \"SamsungPing\"\n            self._ping_thread.daemon = True\n            self._ping_thread_run = True\n            self._ping_thread.start()\n\n    def stop_poll(self):\n        \"\"\"Stop polling the TV for status.\"\"\"\n        if self._ping_thread is not None and not self._ping_thread.is_alive():\n            self._ping_thread_run = False\n            self._ping_thread.join()\n            if self._is_connected:\n                self.stop_client()\n            self._ping_thread = None\n\n    def _start_client(self, *, start_all=False):\n        \"\"\"Start all thread that connect to the TV websocket\"\"\"\n\n        if self._client_remote is None or not self._client_remote.is_alive():\n            self._client_remote = Thread(target=self._client_remote_thread)\n            self._client_remote.name = \"SamsungRemote\"\n            self._client_remote.daemon = True\n            self._client_remote.start()\n\n            return\n\n        if start_all:\n            if self._client_control is None or not self._client_control.is_alive():\n                self._client_control = Thread(target=self._client_control_thread)\n                self._client_control.name = \"SamsungControl\"\n                self._client_control.daemon = True\n                self._client_control.start()\n\n            if self._client_art_supported > 0 and (\n                self._client_art is None or not self._client_art.is_alive()\n            ):\n                if self._client_art_supported > 1:\n                    self._client_art_supported = 0\n                self._client_art = Thread(target=self._client_art_thread)\n                self._client_art.name = \"SamsungArt\"\n                self._client_art.daemon = True\n                self._client_art.start()\n\n    def stop_client(self):\n        \"\"\"Stop the ws remote client thread.\"\"\"\n        if self._ws_remote:\n            self._ws_remote.close()\n\n    def open(self):\n        \"\"\"Open a WS client connection with the TV.\"\"\"\n        if self.connection is not None:\n            return self.connection\n\n        is_ssl = self._is_ssl_connection()\n        url = self._format_websocket_url(_WS_ENDPOINT_REMOTE_CONTROL, is_ssl=is_ssl)\n        sslopt = {\"cert_reqs\": ssl.CERT_NONE} if is_ssl else {}\n\n        _LOGGING.debug(\"WS url %s\", url)\n        connection = websocket.create_connection(url, self.timeout, sslopt=sslopt)\n        completed = False\n        response = \"\"\n\n        for _ in range(3):\n            response = _process_api_response(connection.recv())\n            _LOGGING.debug(response)\n            event = response.get(\"event\", \"-\")\n            if event != \"ms.channel.connect\":\n                break\n            conn_data = response.get(\"data\")\n            if self._check_conn_id(conn_data):\n                completed = True\n                token = conn_data.get(\"token\")\n                if token:\n                    self._set_token(token)\n                break\n\n        if not completed:\n            self.close()\n            raise ConnectionFailure(response)\n\n        self.connection = connection\n        return connection\n\n    def close(self):\n        \"\"\"Close WS connection.\"\"\"\n        if self.connection:\n            self.connection.close()\n            _LOGGING.debug(\"Connection closed.\")\n        self.connection = None\n\n    def send_key(self, key, key_press_delay=None, cmd=\"Click\"):\n        \"\"\"Send a key to the TV using appropriate WS connection.\"\"\"\n        _LOGGING.debug(\"Sending key %s\", key)\n        return self._ws_send(\n            {\n                \"method\": \"ms.remote.control\",\n                \"params\": {\n                    \"Cmd\": cmd,\n                    \"DataOfCmd\": key,\n                    \"Option\": \"false\",\n                    \"TypeOfRemote\": \"SendRemoteKey\",\n                },\n            },\n            key_press_delay,\n        )\n\n    def hold_key(self, key, seconds):\n        \"\"\"Send a key to the TV and keep it pressed for specific number of seconds\"\"\"\n        if self.send_key(key, key_press_delay=0, cmd=\"Press\"):\n            time.sleep(seconds)\n            return self.send_key(key, key_press_delay=0, cmd=\"Release\")\n        return False\n\n    def send_text(self, text, send_delay=None):\n        \"\"\"Send a text string to the TV.\"\"\"\n        if not text:\n            return False\n\n        base64_text = self._serialize_string(text)\n        if self._ws_send(\n            {\n                \"method\": \"ms.remote.control\",\n                \"params\": {\n                    \"Cmd\": f\"{base64_text}\",\n                    \"DataOfCmd\": \"base64\",\n                    \"TypeOfRemote\": \"SendInputString\",\n                },\n            },\n            key_press_delay=send_delay,\n        ):\n            self._ws_send(\n                {\n                    \"method\": \"ms.remote.control\",\n                    \"params\": {\n                        \"TypeOfRemote\": \"SendInputEnd\",\n                    },\n                },\n                key_press_delay=0,\n            )\n            return True\n\n        return False\n\n    def move_cursor(self, x, y, duration=0):\n        \"\"\"Move the cursor in the TV to specific coordinate.\"\"\"\n        self._ws_send(\n            {\n                \"method\": \"ms.remote.control\",\n                \"params\": {\n                    \"Cmd\": \"Move\",\n                    \"Position\": {\"x\": x, \"y\": y, \"Time\": str(duration)},\n                    \"TypeOfRemote\": \"ProcessMouseDevice\",\n                },\n            },\n            key_press_delay=0,\n        )\n\n    def run_app(self, app_id, action_type=\"\", meta_tag=\"\", *, use_remote=False):\n        \"\"\"Launch an app using appropriate WS channel.\"\"\"\n        if not action_type:\n            app = self._installed_app.get(app_id)\n            if app:\n                app_type = app.app_type\n            else:\n                app_type = self._app_type.get(app_id, 2)\n            action_type = TYPE_DEEP_LINK if app_type == 2 else TYPE_NATIVE_LAUNCH\n        elif action_type != TYPE_NATIVE_LAUNCH:\n            action_type = TYPE_DEEP_LINK\n\n        _LOGGING.debug(\n            \"Sending run app app_id: %s app_type: %s meta_tag: %s\",\n            app_id,\n            action_type,\n            meta_tag,\n        )\n\n        if self._ws_control and action_type == TYPE_DEEP_LINK and not use_remote:\n            return self._ws_send(\n                {\n                    \"id\": app_id,\n                    \"method\": \"ms.application.start\",\n                    \"params\": {\"id\": app_id},\n                },\n                key_press_delay=0,\n                use_control=True,\n                ws_socket=self._ws_control,\n            )\n\n        return self._ws_send(\n            {\n                \"method\": \"ms.channel.emit\",\n                \"params\": {\n                    \"event\": \"ed.apps.launch\",\n                    \"to\": \"host\",\n                    \"data\": {\n                        # action_type: NATIVE_LAUNCH / DEEP_LINK\n                        # app_type == 2 ? 'DEEP_LINK' : 'NATIVE_LAUNCH',\n                        \"action_type\": action_type,\n                        \"appId\": app_id,\n                        \"metaTag\": meta_tag,\n                    },\n                },\n            },\n            key_press_delay=0,\n        )\n\n    def open_browser(self, url):\n        \"\"\"Launch the browser app on the TV.\"\"\"\n        _LOGGING.debug(\"Opening url in browser %s\", url)\n        return self.run_app(\"org.tizen.browser\", TYPE_NATIVE_LAUNCH, url)\n\n    def rest_device_info(self):\n        \"\"\"Get device info using rest api call.\"\"\"\n        _LOGGING.debug(\"Get device info via rest api\")\n        return self._rest_request(\"\")\n\n    def rest_app_status(self, app_id):\n        \"\"\"Get app status using rest api call.\"\"\"\n        _LOGGING.debug(\"Get app %s status via rest api\", app_id)\n        return self._rest_request(\"applications/\" + app_id)\n\n    def rest_app_run(self, app_id):\n        \"\"\"Run an app using rest api call.\"\"\"\n        _LOGGING.debug(\"Run app %s via rest api\", app_id)\n        return self._rest_request(\"applications/\" + app_id, \"POST\")\n\n    def rest_app_close(self, app_id):\n        \"\"\"Close an app using rest api call.\"\"\"\n        _LOGGING.debug(\"Close app %s via rest api\", app_id)\n        return self._rest_request(\"applications/\" + app_id, \"DELETE\")\n\n    def rest_app_install(self, app_id):\n        \"\"\"Install a new app using rest api call.\"\"\"\n        _LOGGING.debug(\"Install app %s via rest api\", app_id)\n        return self._rest_request(\"applications/\" + app_id, \"PUT\")\n\n    def shortcuts(self):\n        \"\"\"Return a list of available shortcuts.\"\"\"\n        return SamsungTVShortcuts(self)\n"
  },
  {
    "path": "custom_components/samsungtv_smart/api/shortcuts.py",
    "content": "\"\"\"\nSamsungTVWS - Samsung Smart TV WS API wrapper\n\nCopyright (C) 2019 Xchwarze\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor,\n    Boston, MA  02110-1335  USA\n\n\"\"\"\n\n\nclass SamsungTVShortcuts:\n    def __init__(self, remote):\n        self.remote = remote\n\n    # power\n    def power(self):\n        self.remote.send_key(\"KEY_POWER\")\n\n    # menu\n    def home(self):\n        self.remote.send_key(\"KEY_HOME\")\n\n    def menu(self):\n        self.remote.send_key(\"KEY_MENU\")\n\n    def source(self):\n        self.remote.send_key(\"KEY_SOURCE\")\n\n    def guide(self):\n        self.remote.send_key(\"KEY_GUIDE\")\n\n    def tools(self):\n        self.remote.send_key(\"KEY_TOOLS\")\n\n    def info(self):\n        self.remote.send_key(\"KEY_INFO\")\n\n    # navigation\n    def up(self):\n        self.remote.send_key(\"KEY_UP\")\n\n    def down(self):\n        self.remote.send_key(\"KEY_DOWN\")\n\n    def left(self):\n        self.remote.send_key(\"KEY_LEFT\")\n\n    def right(self):\n        self.remote.send_key(\"KEY_RIGHT\")\n\n    def enter(self, count=1):\n        self.remote.send_key(\"KEY_ENTER\")\n\n    def back(self):\n        self.remote.send_key(\"KEY_RETURN\")\n\n    # channel\n    def channel_list(self):\n        self.remote.send_key(\"KEY_CH_LIST\")\n\n    def channel(self, ch):\n        for c in str(ch):\n            self.digit(c)\n\n        self.enter()\n\n    def digit(self, d):\n        self.remote.send_key(\"KEY_\" + d)\n\n    def channel_up(self):\n        self.remote.send_key(\"KEY_CHUP\")\n\n    def channel_down(self):\n        self.remote.send_key(\"KEY_CHDOWN\")\n\n    # volume\n    def volume_up(self):\n        self.remote.send_key(\"KEY_VOLUP\")\n\n    def volume_down(self):\n        self.remote.send_key(\"KEY_VOLDOWN\")\n\n    def mute(self):\n        self.remote.send_key(\"KEY_MUTE\")\n\n    # extra\n    def red(self):\n        self.remote.send_key(\"KEY_RED\")\n\n    def green(self):\n        self.remote.send_key(\"KEY_GREEN\")\n\n    def yellow(self):\n        self.remote.send_key(\"KEY_YELLOW\")\n\n    def blue(self):\n        self.remote.send_key(\"KEY_BLUE\")\n"
  },
  {
    "path": "custom_components/samsungtv_smart/api/smartthings.py",
    "content": "\"\"\"SmartThings TV integration.\"\"\"\n\nfrom __future__ import annotations\n\nfrom asyncio import TimeoutError as AsyncTimeoutError\nfrom collections.abc import Callable\nfrom datetime import timedelta\nfrom enum import Enum\nimport json\nimport logging\n\nfrom aiohttp import ClientConnectionError, ClientResponseError, ClientSession\n\nfrom homeassistant.util import Throttle\n\nAPI_BASEURL = \"https://api.smartthings.com/v1\"\nAPI_DEVICES = f\"{API_BASEURL}/devices\"\n\nDEVICE_TYPE_OCF = \"OCF\"\nDEVICE_TYPE_NAME_TV = \"Samsung OCF TV\"\nDEVICE_TYPE_NAMES = [\"Samsung OCF TV\", \"x.com.st.d.monitor\"]\n\n\nCOMMAND_POWER_OFF = {\n    \"capability\": \"switch\",\n    \"command\": \"off\",\n}\nCOMMAND_POWER_ON = {\n    \"capability\": \"switch\",\n    \"command\": \"on\",\n}\nCOMMAND_REFRESH = {\n    \"capability\": \"refresh\",\n    \"command\": \"refresh\",\n}\nCOMMAND_SET_SOURCE = {\n    \"capability\": \"mediaInputSource\",\n    \"command\": \"setInputSource\",\n}\nCOMMAND_SET_VD_SOURCE = {\n    \"capability\": \"samsungvd.mediaInputSource\",\n    \"command\": \"setInputSource\",\n}\nCOMMAND_MUTE = {\n    \"capability\": \"audioMute\",\n    \"command\": \"mute\",\n}\nCOMMAND_UNMUTE = {\n    \"capability\": \"audioMute\",\n    \"command\": \"unmute\",\n}\nCOMMAND_VOLUME_UP = {\n    \"capability\": \"audioVolume\",\n    \"command\": \"volumeUp\",\n}\nCOMMAND_VOLUME_DOWN = {\n    \"capability\": \"audioVolume\",\n    \"command\": \"volumeDown\",\n}\nCOMMAND_SET_VOLUME = {\n    \"capability\": \"audioVolume\",\n    \"command\": \"setVolume\",\n}\nCOMMAND_CHANNEL_UP = {\n    \"capability\": \"tvChannel\",\n    \"command\": \"channelUp\",\n}\nCOMMAND_CHANNEL_DOWN = {\n    \"capability\": \"tvChannel\",\n    \"command\": \"channelDown\",\n}\nCOMMAND_SET_CHANNEL = {\n    \"capability\": \"tvChannel\",\n    \"command\": \"setTvChannel\",\n}\nCOMMAND_PAUSE = {\n    \"capability\": \"mediaPlayback\",\n    \"command\": \"pause\",\n}\nCOMMAND_PLAY = {\n    \"capability\": \"mediaPlayback\",\n    \"command\": \"play\",\n}\nCOMMAND_STOP = {\n    \"capability\": \"mediaPlayback\",\n    \"command\": \"stop\",\n}\nCOMMAND_FAST_FORWARD = {\n    \"capability\": \"mediaPlayback\",\n    \"command\": \"fastForward\",\n}\nCOMMAND_REWIND = {\n    \"capability\": \"mediaPlayback\",\n    \"command\": \"rewind\",\n}\nCOMMAND_SOUND_MODE = {\n    \"capability\": \"custom.soundmode\",\n    \"command\": \"setSoundMode\",\n}\nCOMMAND_PICTURE_MODE = {\n    \"capability\": \"custom.picturemode\",\n    \"command\": \"setPictureMode\",\n}\n\nDIGITAL_TV = \"digitalTv\"\n\nMIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)\n_LOGGER = logging.getLogger(__name__)\n\n\ndef _headers(api_key: str) -> dict[str, str]:\n    return {\n        \"Authorization\": f\"Bearer {api_key}\",\n        \"Accept\": \"application/json\",\n        \"Connection\": \"keep-alive\",\n    }\n\n\ndef _command(command: dict, arguments: list | None = None):\n    cmd = {\"component\": \"main\", **command}\n    if arguments:\n        cmd[\"arguments\"] = arguments\n    cmd_full = {\"commands\": [cmd]}\n    return str(cmd_full)\n\n\nclass STStatus(Enum):\n    \"\"\"Represent SmartThings status.\"\"\"\n\n    STATE_OFF = 0\n    STATE_ON = 1\n    STATE_UNKNOWN = 2\n\n\nclass SmartThingsTV:\n    \"\"\"Class to read status for TV registered in SmartThings cloud.\"\"\"\n\n    def __init__(\n        self,\n        api_key: str,\n        device_id: str,\n        use_channel_info: bool = True,\n        session: ClientSession | None = None,\n        api_key_callback: Callable[[], str | None] | None = None,\n    ):\n        \"\"\"Initialize SmartThingsTV.\"\"\"\n        self._api_key = api_key\n        self._device_id = device_id\n        self._use_channel_info = use_channel_info\n        if session:\n            self._session = session\n            self._managed_session = False\n        else:\n            self._session = ClientSession()\n            self._managed_session = True\n\n        self._device_name = None\n        self._state = STStatus.STATE_UNKNOWN\n        self._prev_state = STStatus.STATE_UNKNOWN\n        self._muted = False\n        self._volume = 10\n        self._source_list = None\n        self._source_list_map = None\n        self._source = \"\"\n        self._channel = \"\"\n        self._channel_name = \"\"\n        self._sound_mode = None\n        self._sound_mode_list = None\n        self._picture_mode = None\n        self._picture_mode_list = None\n\n        self._is_forced_val = False\n        self._forced_count = 0\n\n        self._api_key_callback = api_key_callback\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, ext_type, ext_value, ext_traceback):\n        pass\n\n    def _get_api_key(self) -> str:\n        \"\"\"Get API key used to connect to smartthink.\"\"\"\n        if self._api_key_callback is not None:\n            if api_key := self._api_key_callback():\n                self._api_key = api_key\n        return self._api_key\n\n    @property\n    def api_key(self) -> str:\n        \"\"\"Return current api_key.\"\"\"\n        return self._api_key\n\n    @property\n    def device_id(self) -> str:\n        \"\"\"Return current device_id.\"\"\"\n        return self._device_id\n\n    @property\n    def device_name(self) -> str:\n        \"\"\"Return current device_name.\"\"\"\n        return self._device_name\n\n    @property\n    def state(self):\n        \"\"\"Return current state.\"\"\"\n        return self._state\n\n    @property\n    def prev_state(self):\n        \"\"\"Return current state.\"\"\"\n        return self._prev_state\n\n    @property\n    def muted(self) -> bool:\n        \"\"\"Return current muted state.\"\"\"\n        return self._muted\n\n    @property\n    def volume(self) -> int:\n        \"\"\"Return current volume.\"\"\"\n        return self._volume\n\n    @property\n    def source(self) -> str:\n        \"\"\"Return current source.\"\"\"\n        return self._source\n\n    @property\n    def channel(self) -> str:\n        \"\"\"Return current channel.\"\"\"\n        return self._channel\n\n    @property\n    def channel_name(self) -> str:\n        \"\"\"Return current channel name.\"\"\"\n        return self._channel_name\n\n    @property\n    def source_list(self):\n        \"\"\"Return available source list.\"\"\"\n        return self._source_list\n\n    @property\n    def sound_mode(self):\n        \"\"\"Return current sound mode.\"\"\"\n        if self._state != STStatus.STATE_ON:\n            return None\n        return self._sound_mode\n\n    @property\n    def sound_mode_list(self):\n        \"\"\"Return available sound modes.\"\"\"\n        if self._state != STStatus.STATE_ON:\n            return None\n        return self._sound_mode_list\n\n    @property\n    def picture_mode(self):\n        \"\"\"Return current picture mode.\"\"\"\n        if self._state != STStatus.STATE_ON:\n            return None\n        return self._picture_mode\n\n    @property\n    def picture_mode_list(self):\n        \"\"\"Return available picture modes.\"\"\"\n        if self._state != STStatus.STATE_ON:\n            return None\n        return self._picture_mode_list\n\n    def get_source_name(self, source_id: str) -> str:\n        \"\"\"Get source name based on source id.\"\"\"\n        if not self._source_list_map:\n            return \"\"\n        if source_id.upper() == DIGITAL_TV.upper():\n            source_id = \"dtv\"\n        for map_value in self._source_list_map:\n            map_id = map_value.get(\"id\")\n            if map_id and map_id == source_id:\n                return map_value.get(\"name\", \"\")\n        return \"\"\n\n    def _get_source_list_from_map(self) -> list:\n        \"\"\"Return source list from source map.\"\"\"\n        if not self._source_list_map:\n            return []\n        source_list = []\n        for map_value in self._source_list_map:\n            if source_id := map_value.get(\"id\"):\n                if source_id.upper() == \"DTV\":\n                    source_list.append(DIGITAL_TV)\n                else:\n                    source_list.append(source_id)\n        return source_list\n\n    def set_application(self, app_id):\n        \"\"\"Set running application info.\"\"\"\n        if self._use_channel_info:\n            self._channel = \"\"\n            self._channel_name = app_id\n            self._is_forced_val = True\n            self._forced_count = 0\n\n    def _set_source(self, source):\n        \"\"\"Set current source info.\"\"\"\n        if source != self._source:\n            self._source = source\n            self._channel = \"\"\n            self._channel_name = \"\"\n            self._is_forced_val = True\n            self._forced_count = 0\n\n    @staticmethod\n    def _load_json_list(dev_data, list_name):\n        \"\"\"Try load a list from string to json format.\"\"\"\n        load_list = []\n        json_list = dev_data.get(list_name, {}).get(\"value\")\n        if json_list:\n            try:\n                load_list = json.loads(json_list)\n            except (TypeError, ValueError):\n                pass\n        return load_list\n\n    @staticmethod\n    async def get_devices_list(api_key, session: ClientSession, device_label=\"\"):\n        \"\"\"Get list of available SmartThings devices\"\"\"\n\n        result = {}\n\n        async with session.get(\n            API_DEVICES,\n            headers=_headers(api_key),\n            raise_for_status=True,\n        ) as resp:\n            device_list = await resp.json()\n\n        if device_list:\n            _LOGGER.debug(\"SmartThings available devices: %s\", str(device_list))\n\n            for dev in device_list.get(\"items\", []):\n                if (device_id := dev.get(\"deviceId\")) is None:\n                    continue\n                if dev.get(\"type\", \"\") != DEVICE_TYPE_OCF:\n                    continue\n\n                label = dev.get(\"label\", \"\")\n                if device_label:\n                    if label != device_label:\n                        continue\n                elif dev.get(\"deviceTypeName\", \"\") not in DEVICE_TYPE_NAMES:\n                    continue\n\n                result[device_id] = {\n                    \"name\": dev.get(\"name\", f\"TV ID {device_id}\"),\n                    \"label\": label,\n                }\n\n        _LOGGER.info(\"SmartThings discovered TV devices: %s\", str(result))\n\n        return result\n\n    @Throttle(MIN_TIME_BETWEEN_UPDATES)\n    async def _device_refresh(self, **kwargs):\n        \"\"\"Refresh device status on SmartThings\"\"\"\n\n        device_id = self._device_id\n        if not device_id:\n            return\n\n        api_device = f\"{API_DEVICES}/{device_id}\"\n        api_command = f\"{api_device}/commands\"\n\n        if self._use_channel_info:\n            async with self._session.post(\n                api_command,\n                headers=_headers(self._get_api_key()),\n                data=_command(COMMAND_REFRESH),\n                raise_for_status=False,\n            ) as resp:\n                if resp.status == 409:\n                    self._state = STStatus.STATE_OFF\n                    return\n                resp.raise_for_status()\n                await resp.json()\n\n        return\n\n    async def _async_send_command(self, data_cmd):\n        \"\"\"Send a command via SmartThings\"\"\"\n        device_id = self._device_id\n        if not device_id:\n            return\n        if not data_cmd:\n            return\n\n        api_device = f\"{API_DEVICES}/{device_id}\"\n        api_command = f\"{api_device}/commands\"\n\n        async with self._session.post(\n            api_command,\n            headers=_headers(self._get_api_key()),\n            data=data_cmd,\n            raise_for_status=True,\n        ) as resp:\n            await resp.json()\n\n        await self._device_refresh()\n\n    async def async_device_health(self):\n        \"\"\"Check device availability\"\"\"\n\n        device_id = self._device_id\n        if not device_id:\n            return False\n\n        api_device = f\"{API_DEVICES}/{device_id}\"\n        api_device_health = f\"{api_device}/health\"\n\n        # this get the real status of the device\n        async with self._session.get(\n            api_device_health,\n            headers=_headers(self._get_api_key()),\n            raise_for_status=True,\n        ) as resp:\n            health = await resp.json()\n\n        _LOGGER.debug(health)\n\n        if health[\"state\"] == \"ONLINE\":\n            return True\n        return False\n\n    async def async_device_update(self, use_channel_info: bool = None):\n        \"\"\"Query device status on SmartThing\"\"\"\n\n        device_id = self._device_id\n        if not device_id:\n            return\n\n        if use_channel_info is not None:\n            self._use_channel_info = use_channel_info\n\n        api_device = f\"{API_DEVICES}/{device_id}\"\n        api_device_status = f\"{api_device}/states\"\n        # not used, just for reference\n        # api_device_main_status = f\"{api_device}/components/main/status\"\n\n        self._prev_state = self._state\n\n        try:\n            is_online = await self.async_device_health()\n        except (\n            AsyncTimeoutError,\n            ClientConnectionError,\n            ClientResponseError,\n        ):\n            self._state = STStatus.STATE_UNKNOWN\n            return\n\n        if is_online:\n            self._state = STStatus.STATE_ON\n        else:\n            self._state = STStatus.STATE_OFF\n            return\n\n        await self._device_refresh()\n        if self._state == STStatus.STATE_OFF:\n            return\n\n        async with self._session.get(\n            api_device_status,\n            headers=_headers(self._get_api_key()),\n            raise_for_status=True,\n        ) as resp:\n            data = await resp.json()\n\n        _LOGGER.debug(data)\n\n        dev_data = data.get(\"main\", {})\n        # device_state = data['main']['switch']['value']\n\n        # Volume\n        device_volume = dev_data.get(\"volume\", {}).get(\"value\", 0)\n        if device_volume and device_volume.isdigit():\n            self._volume = int(device_volume) / 100\n        else:\n            self._volume = 0\n\n        # Muted state\n        device_muted = dev_data.get(\"mute\", {}).get(\"value\", \"\")\n        self._muted = device_muted == \"mute\"\n\n        # Sound Mode\n        self._sound_mode = dev_data.get(\"soundMode\", {}).get(\"value\")\n        self._sound_mode_list = self._load_json_list(dev_data, \"supportedSoundModes\")\n\n        # Picture Mode\n        self._picture_mode = dev_data.get(\"pictureMode\", {}).get(\"value\")\n        self._picture_mode_list = self._load_json_list(\n            dev_data, \"supportedPictureModes\"\n        )\n\n        # Sources and channel\n        self._source_list_map = self._load_json_list(\n            dev_data, \"supportedInputSourcesMap\"\n        )\n        # self._source_list = self._load_json_list(dev_data, \"supportedInputSources\")\n        self._source_list = self._get_source_list_from_map()\n\n        if self._is_forced_val and self._forced_count <= 0:\n            self._forced_count += 1\n            return\n        self._is_forced_val = False\n        self._forced_count = 0\n\n        device_source = dev_data.get(\"inputSource\", {}).get(\"value\", \"\")\n        device_tv_chan = dev_data.get(\"tvChannel\", {}).get(\"value\", \"\")\n        device_tv_chan_name = dev_data.get(\"tvChannelName\", {}).get(\"value\", \"\")\n\n        if device_source:\n            if device_source.upper() == DIGITAL_TV.upper():\n                device_source = DIGITAL_TV\n        self._source = device_source\n        # if the status is not refreshed this info may become not reliable\n        if self._use_channel_info:\n            self._channel = device_tv_chan\n            self._channel_name = device_tv_chan_name\n        else:\n            self._channel = \"\"\n            self._channel_name = \"\"\n\n    async def async_turn_off(self):\n        \"\"\"Turn off TV via SmartThings\"\"\"\n        data_cmd = _command(COMMAND_POWER_OFF)\n        await self._async_send_command(data_cmd)\n\n    async def async_turn_on(self):\n        \"\"\"Turn on TV via SmartThings\"\"\"\n        data_cmd = _command(COMMAND_POWER_ON)\n        await self._async_send_command(data_cmd)\n\n    async def async_send_command(self, cmd_type, command=\"\"):\n        \"\"\"Send a command to the device\"\"\"\n        data_cmd = None\n\n        if cmd_type == \"setvolume\":  # sets volume\n            data_cmd = _command(COMMAND_SET_VOLUME, [int(command)])\n        elif cmd_type == \"stepvolume\":  # steps volume up or down\n            if command == \"up\":\n                data_cmd = _command(COMMAND_VOLUME_UP)\n            elif command == \"down\":\n                data_cmd = _command(COMMAND_VOLUME_DOWN)\n        elif cmd_type == \"audiomute\":  # mutes audio\n            if command == \"on\":\n                data_cmd = _command(COMMAND_MUTE)\n            elif command == \"off\":\n                data_cmd = _command(COMMAND_UNMUTE)\n        elif cmd_type == \"selectchannel\":  # changes channel\n            data_cmd = _command(COMMAND_SET_CHANNEL, [command])\n        elif cmd_type == \"stepchannel\":  # steps channel up or down\n            if command == \"up\":\n                data_cmd = _command(COMMAND_CHANNEL_UP)\n            elif command == \"down\":\n                data_cmd = _command(COMMAND_CHANNEL_DOWN)\n        else:\n            return\n\n        await self._async_send_command(data_cmd)\n\n    async def async_select_source(self, source):\n        \"\"\"Select source\"\"\"\n        # if source not in self._source_list:\n        #     return\n        data_cmd = _command(COMMAND_SET_SOURCE, [source])\n        # set property to reflect new changes\n        self._set_source(source)\n        await self._async_send_command(data_cmd)\n\n    async def async_select_vd_source(self, source):\n        \"\"\"Select source\"\"\"\n        # if source not in self._source_list:\n        #     return\n        data_cmd = _command(COMMAND_SET_VD_SOURCE, [source])\n        await self._async_send_command(data_cmd)\n\n    async def async_set_sound_mode(self, mode):\n        \"\"\"Select sound mode\"\"\"\n        if self._state != STStatus.STATE_ON:\n            return\n        if mode not in self._sound_mode_list:\n            raise InvalidSmartThingsSoundMode()\n        data_cmd = _command(COMMAND_SOUND_MODE, [mode])\n        await self._async_send_command(data_cmd)\n        self._sound_mode = mode\n\n    async def async_set_picture_mode(self, mode):\n        \"\"\"Select picture mode\"\"\"\n        if self._state != STStatus.STATE_ON:\n            return\n        if mode not in self._picture_mode_list:\n            raise InvalidSmartThingsPictureMode()\n        data_cmd = _command(COMMAND_PICTURE_MODE, [mode])\n        await self._async_send_command(data_cmd)\n        self._picture_mode = mode\n\n\nclass InvalidSmartThingsSoundMode(RuntimeError):\n    \"\"\"Selected sound mode is invalid.\"\"\"\n\n\nclass InvalidSmartThingsPictureMode(RuntimeError):\n    \"\"\"Selected picture mode is invalid.\"\"\"\n"
  },
  {
    "path": "custom_components/samsungtv_smart/api/upnp.py",
    "content": "\"\"\"Smartthings TV integration UPnP implementation.\"\"\"\n\nimport logging\nfrom typing import Optional\nimport xml.etree.ElementTree as ET\n\nfrom aiohttp import ClientSession\nimport async_timeout\n\nDEFAULT_TIMEOUT = 0.2\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass SamsungUPnP:\n    \"\"\"UPnP implementation for Samsung TV.\"\"\"\n\n    def __init__(self, host, session: Optional[ClientSession] = None):\n        \"\"\"Initialize the class.\"\"\"\n        self._host = host\n        self._connected = False\n        if session:\n            self._session = session\n            self._managed_session = False\n        else:\n            self._session = ClientSession()\n            self._managed_session = True\n\n    async def _soap_request(\n        self, action, arguments, protocole, *, timeout=DEFAULT_TIMEOUT\n    ):\n        \"\"\"Send a SOAP request to the TV.\"\"\"\n        headers = {\n            \"SOAPAction\": f'\"urn:schemas-upnp-org:service:{protocole}:1#{action}\"',\n            \"content-type\": \"text/xml\",\n        }\n        body = f\"\"\"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n                <s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n                    <s:Body>\n                    <u:{action} xmlns:u=\"urn:schemas-upnp-org:service:{protocole}:1\">\n                        <InstanceID>0</InstanceID>\n                        {arguments}\n                    </u:{action}>\n                    </s:Body>\n                </s:Envelope>\"\"\"\n        try:\n            async with async_timeout.timeout(timeout):\n                async with self._session.post(\n                    f\"http://{self._host}:9197/upnp/control/{protocole}1\",\n                    headers=headers,\n                    data=body,\n                    raise_for_status=True,\n                ) as resp:\n                    response = await resp.content.read()\n                    self._connected = True\n        except Exception as exc:  # pylint: disable=broad-except\n            _LOGGER.debug(exc)\n            self._connected = False\n            return None\n\n        return response\n\n    @property\n    def connected(self):\n        \"\"\"Return if connected to Samsung TV.\"\"\"\n        return self._connected\n\n    async def async_disconnect(self):\n        \"\"\"Disconnect from TV and close session.\"\"\"\n        if self._managed_session:\n            await self._session.close()\n\n    async def async_get_volume(self):\n        \"\"\"Return volume status.\"\"\"\n        response = await self._soap_request(\n            \"GetVolume\", \"<Channel>Master</Channel>\", \"RenderingControl\"\n        )\n        if response is None:\n            return None\n\n        tree = ET.fromstring(response.decode(\"utf8\"))\n        volume = None\n        for elem in tree.iter(tag=\"CurrentVolume\"):\n            volume = elem.text\n        return volume\n\n    async def async_set_volume(self, volume):\n        \"\"\"Set the volume level.\"\"\"\n        await self._soap_request(\n            \"SetVolume\",\n            f\"<Channel>Master</Channel><DesiredVolume>{volume}</DesiredVolume>\",\n            \"RenderingControl\",\n        )\n\n    async def async_get_mute(self):\n        \"\"\"Return mute status.\"\"\"\n        response = await self._soap_request(\n            \"GetMute\", \"<Channel>Master</Channel>\", \"RenderingControl\"\n        )\n        if response is None:\n            return None\n\n        tree = ET.fromstring(response.decode(\"utf8\"))\n        mute = None\n        for elem in tree.iter(tag=\"CurrentMute\"):\n            mute = elem.text\n        if mute is None:\n            return None\n        return int(mute) != 0\n\n    async def async_set_current_media(self, url):\n        \"\"\"Set media to playback and play it.\"\"\"\n\n        if (\n            await self._soap_request(\n                \"SetAVTransportURI\",\n                f\"<CurrentURI>{url}</CurrentURI><CurrentURIMetaData></CurrentURIMetaData>\",\n                \"AVTransport\",\n                timeout=2.0,\n            )\n            is None\n        ):\n            return False\n\n        await self._soap_request(\"Play\", \"<Speed>1</Speed>\", \"AVTransport\")\n        return True\n\n    async def async_play(self):\n        \"\"\"Play media that was already set as current.\"\"\"\n        await self._soap_request(\"Play\", \"<Speed>1</Speed>\", \"AVTransport\")\n"
  },
  {
    "path": "custom_components/samsungtv_smart/config_flow.py",
    "content": "\"\"\"Config flow for Samsung TV.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom numbers import Number\nimport socket\nfrom typing import Any, Dict\n\nimport voluptuous as vol\n\nfrom homeassistant.components.binary_sensor import DOMAIN as BS_DOMAIN\nfrom homeassistant.config_entries import (\n    SOURCE_RECONFIGURE,\n    SOURCE_USER,\n    ConfigEntry,\n    ConfigFlow,\n    ConfigFlowResult,\n    OptionsFlow,\n)\nfrom homeassistant.const import (\n    ATTR_DEVICE_ID,\n    CONF_API_KEY,\n    CONF_BASE,\n    CONF_DEVICE_ID,\n    CONF_HOST,\n    CONF_ID,\n    CONF_MAC,\n    CONF_NAME,\n    CONF_PORT,\n    CONF_TOKEN,\n    SERVICE_TURN_ON,\n    __version__,\n)\nfrom homeassistant.core import HomeAssistant, callback\nfrom homeassistant.helpers import entity_registry as er\nfrom homeassistant.helpers.aiohttp_client import async_get_clientsession\nfrom homeassistant.helpers.selector import (\n    EntitySelector,\n    EntitySelectorConfig,\n    ObjectSelector,\n    SelectOptionDict,\n    SelectSelector,\n    SelectSelectorConfig,\n    SelectSelectorMode,\n)\n\nfrom . import (\n    SamsungTVInfo,\n    get_device_info,\n    get_smartthings_api_key,\n    get_smartthings_entries,\n    is_valid_ha_version,\n)\nfrom .const import (\n    ATTR_DEVICE_MAC,\n    ATTR_DEVICE_MODEL,\n    ATTR_DEVICE_NAME,\n    ATTR_DEVICE_OS,\n    CONF_APP_LAUNCH_METHOD,\n    CONF_APP_LIST,\n    CONF_APP_LOAD_METHOD,\n    CONF_CHANNEL_LIST,\n    CONF_DEVICE_MODEL,\n    CONF_DEVICE_NAME,\n    CONF_DEVICE_OS,\n    CONF_DUMP_APPS,\n    CONF_EXT_POWER_ENTITY,\n    CONF_LOGO_OPTION,\n    CONF_PING_PORT,\n    CONF_POWER_ON_METHOD,\n    CONF_SHOW_CHANNEL_NR,\n    CONF_SOURCE_LIST,\n    CONF_ST_ENTRY_UNIQUE_ID,\n    CONF_SYNC_TURN_OFF,\n    CONF_SYNC_TURN_ON,\n    CONF_TOGGLE_ART_MODE,\n    CONF_USE_LOCAL_LOGO,\n    CONF_USE_MUTE_CHECK,\n    CONF_USE_ST_CHANNEL_INFO,\n    CONF_USE_ST_STATUS_INFO,\n    CONF_WOL_REPEAT,\n    CONF_WS_NAME,\n    DOMAIN,\n    MAX_WOL_REPEAT,\n    RESULT_ST_DEVICE_NOT_FOUND,\n    RESULT_ST_DEVICE_USED,\n    RESULT_SUCCESS,\n    RESULT_WRONG_APIKEY,\n    AppLaunchMethod,\n    AppLoadMethod,\n    PowerOnMethod,\n    __min_ha_version__,\n)\nfrom .logo import LOGO_OPTION_DEFAULT, LogoOption\n\nAPP_LAUNCH_METHODS = {\n    AppLaunchMethod.Standard.value: \"Control Web Socket Channel\",\n    AppLaunchMethod.Remote.value: \"Remote Web Socket Channel\",\n    AppLaunchMethod.Rest.value: \"Rest API Call\",\n}\n\nAPP_LOAD_METHODS = {\n    AppLoadMethod.All.value: \"All Apps\",\n    AppLoadMethod.Default.value: \"Default Apps\",\n    AppLoadMethod.NotLoad.value: \"Not Load\",\n}\n\nLOGO_OPTIONS = {\n    LogoOption.Disabled.value: \"Disabled\",\n    LogoOption.WhiteColor.value: \"White background, Color logo\",\n    LogoOption.BlueColor.value: \"Blue background, Color logo\",\n    LogoOption.BlueWhite.value: \"Blue background, White logo\",\n    LogoOption.DarkWhite.value: \"Dark background, White logo\",\n    LogoOption.TransparentColor.value: \"Transparent background, Color logo\",\n    LogoOption.TransparentWhite.value: \"Transparent background, White logo\",\n}\n\nPOWER_ON_METHODS = {\n    PowerOnMethod.WOL.value: \"WOL Packet (better for wired connection)\",\n    PowerOnMethod.SmartThings.value: \"SmartThings (better for wireless connection)\",\n}\n\nCONF_SHOW_ADV_OPT = \"show_adv_opt\"\nCONF_ST_DEVICE = \"st_devices\"\nCONF_USE_HA_NAME = \"use_ha_name_for_ws\"\n\nADVANCED_OPTIONS = [\n    CONF_APP_LAUNCH_METHOD,\n    CONF_DUMP_APPS,\n    CONF_EXT_POWER_ENTITY,\n    CONF_PING_PORT,\n    CONF_WOL_REPEAT,\n    CONF_TOGGLE_ART_MODE,\n    CONF_USE_MUTE_CHECK,\n]\n\nENUM_OPTIONS = [\n    CONF_APP_LOAD_METHOD,\n    CONF_APP_LAUNCH_METHOD,\n    CONF_LOGO_OPTION,\n    CONF_POWER_ON_METHOD,\n]\n\n_LOGGER = logging.getLogger(__name__)\n\n\ndef _get_ip(host):\n    if host is None:\n        return None\n    try:\n        return socket.gethostbyname(host)\n    except socket.gaierror:\n        return None\n\n\nclass SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):\n    \"\"\"Handle a Samsung TV config flow.\"\"\"\n\n    VERSION = 1\n\n    # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167\n\n    def __init__(self) -> None:\n        \"\"\"Initialize flow.\"\"\"\n        self._user_data = None\n        self._st_devices_schema = None\n\n        self._tv_info: SamsungTVInfo | None = None\n        self._host = None\n        self._api_key = None\n        self._st_entry_unique_id = None\n        self._device_id = None\n        self._name = None\n        self._ws_name = None\n        self._logo_option = None\n        self._device_info = {}\n        self._token = None\n        self._ping_port = None\n        self._error: str | None = None\n\n    def _stdev_already_used(self, devices_id) -> bool:\n        \"\"\"Check if a device_id is in HA config.\"\"\"\n        for entry in self._async_current_entries():\n            if entry.data.get(CONF_DEVICE_ID, \"\") == devices_id:\n                return True\n        return False\n\n    def _remove_stdev_used(self, devices_list: Dict[str, Any]) -> Dict[str, Any]:\n        \"\"\"Remove entry already used\"\"\"\n        res_dev_list = devices_list.copy()\n\n        for dev_id in devices_list.keys():\n            if self._stdev_already_used(dev_id):\n                res_dev_list.pop(dev_id)\n        return res_dev_list\n\n    @staticmethod\n    def _extract_dev_name(device) -> str:\n        \"\"\"Extract device neme from SmartThings Info\"\"\"\n        name = device[\"name\"]\n        label = device.get(\"label\", \"\")\n        if label:\n            name += f\" ({label})\"\n        return name\n\n    def _prepare_dev_schema(self, devices_list) -> vol.Schema:\n        \"\"\"Prepare the schema for select correct ST device\"\"\"\n        validate = {}\n        for dev_id, infos in devices_list.items():\n            device_name = self._extract_dev_name(infos)\n            validate[dev_id] = device_name\n        return vol.Schema({vol.Required(CONF_ST_DEVICE): vol.In(validate)})\n\n    async def _get_st_deviceid(self, st_device_label=\"\") -> str:\n        \"\"\"Try to detect SmartThings device id.\"\"\"\n        session = async_get_clientsession(self.hass)\n        devices_list = await SamsungTVInfo.get_st_devices(\n            self._api_key, session, st_device_label\n        )\n        if devices_list is None:\n            return RESULT_WRONG_APIKEY\n\n        devices_list = self._remove_stdev_used(devices_list)\n        if devices_list:\n            if len(devices_list) > 1:\n                self._st_devices_schema = self._prepare_dev_schema(devices_list)\n            else:\n                self._device_id = list(devices_list.keys())[0]\n\n        return RESULT_SUCCESS\n\n    async def _try_connect(self, *, port=None, token=None, skip_info=False) -> str:\n        \"\"\"Try to connect and check auth.\"\"\"\n        self._tv_info = SamsungTVInfo(self.hass, self._host, self._ws_name)\n\n        session = async_get_clientsession(self.hass)\n        result = await self._tv_info.try_connect(\n            session, self._api_key, self._device_id, ws_port=port, ws_token=token\n        )\n        if result == RESULT_SUCCESS:\n            self._token = self._tv_info.ws_token\n            self._ping_port = self._tv_info.ping_port\n            if not skip_info:\n                self._device_info = await get_device_info(self._host, session)\n\n        return result\n\n    @callback\n    def _get_api_key(self) -> str | None:\n        \"\"\"Get api key in configured entries if available.\"\"\"\n        for entry in self._async_current_entries():\n            if CONF_API_KEY in entry.data:\n                if not entry.data.get(CONF_ST_ENTRY_UNIQUE_ID):\n                    return entry.data[CONF_API_KEY]\n        return None\n\n    async def async_step_user(\n        self, user_input: dict[str, Any] | None = None\n    ) -> ConfigFlowResult:\n        \"\"\"Handle a flow initialized by the user.\"\"\"\n\n        if not is_valid_ha_version():\n            return self.async_abort(\n                reason=\"unsupported_version\",\n                description_placeholders={\n                    \"req_ver\": __min_ha_version__,\n                    \"run_ver\": __version__,\n                },\n            )\n\n        if not self._user_data:\n            if api_key := self._get_api_key():\n                self._user_data = {CONF_API_KEY: api_key}\n\n        if user_input is None:\n            return self._show_form()\n\n        self._user_data = user_input\n        ip_address = await self.hass.async_add_executor_job(\n            _get_ip, user_input[CONF_HOST]\n        )\n        if not ip_address:\n            return self._show_form(errors=\"invalid_host\")\n\n        self._async_abort_entries_match({CONF_HOST: ip_address})\n\n        self._host = ip_address\n        self._name = user_input[CONF_NAME]\n        api_key = user_input.get(CONF_API_KEY)\n        st_entry_unique_id = user_input.get(CONF_ST_ENTRY_UNIQUE_ID)\n        if api_key and st_entry_unique_id:\n            return self._show_form(errors=\"only_key_or_st\")\n\n        self._st_entry_unique_id = None\n        if st_entry_unique_id:\n            if not (api_key := get_smartthings_api_key(self.hass, st_entry_unique_id)):\n                return self._show_form(errors=\"st_api_key_fail\")\n            self._st_entry_unique_id = st_entry_unique_id\n\n        self._api_key = api_key\n\n        use_ha_name = user_input.get(CONF_USE_HA_NAME, False)\n        if use_ha_name:\n            ha_conf = self.hass.config\n            if hasattr(ha_conf, \"location_name\"):\n                self._ws_name = ha_conf.location_name\n        if not self._ws_name:\n            self._ws_name = self._name\n\n        result = RESULT_SUCCESS\n        if self._api_key:\n            result = await self._get_st_deviceid()\n\n            if result == RESULT_SUCCESS and not self._device_id:\n                if self._st_devices_schema:\n                    return await self.async_step_stdevice()\n                return await self.async_step_stdeviceid()\n\n        if result == RESULT_SUCCESS:\n            result = await self._try_connect()\n\n        return await self._manage_result(result, True)\n\n    async def async_step_stdevice(\n        self, user_input: dict[str, Any] | None = None\n    ) -> ConfigFlowResult:\n        \"\"\"Handle a flow to select ST device.\"\"\"\n        if user_input is None:\n            return self._show_form(step_id=\"stdevice\")\n\n        self._device_id = user_input.get(CONF_ST_DEVICE)\n\n        result = await self._try_connect()\n        return await self._manage_result(result)\n\n    async def async_step_stdeviceid(\n        self, user_input: dict[str, Any] | None = None\n    ) -> ConfigFlowResult:\n        \"\"\"Handle a flow to manual input a ST device.\"\"\"\n        if user_input is None:\n            return self._show_form(step_id=\"stdeviceid\")\n\n        device_id = user_input.get(CONF_DEVICE_ID)\n        if self._stdev_already_used(device_id):\n            return self._show_form(errors=RESULT_ST_DEVICE_USED, step_id=\"stdeviceid\")\n\n        self._device_id = device_id\n\n        result = await self._try_connect()\n        if result == RESULT_ST_DEVICE_NOT_FOUND:\n            return self._show_form(errors=result, step_id=\"stdeviceid\")\n        return await self._manage_result(result)\n\n    async def async_step_reconfigure(\n        self, user_input: dict[str, Any] | None = None\n    ) -> ConfigFlowResult:\n        \"\"\"Handle reconfiguration of the integration.\"\"\"\n        entry = self._get_reconfigure_entry()\n        if entry.unique_id == entry.data[CONF_HOST]:\n            return self.async_abort(reason=\"host_unique_id\")\n\n        if not self._ws_name:\n            self._ws_name = entry.data[CONF_WS_NAME]\n            if CONF_API_KEY in entry.data:\n                self._device_id = entry.data.get(CONF_DEVICE_ID)\n\n        if user_input is None:\n            return self._show_form(errors=None, step_id=SOURCE_RECONFIGURE)\n\n        ip_address = await self.hass.async_add_executor_job(\n            _get_ip, user_input[CONF_HOST]\n        )\n        if not ip_address:\n            return self._show_form(errors=\"invalid_host\", step_id=SOURCE_RECONFIGURE)\n\n        self._async_abort_entries_match({CONF_HOST: ip_address})\n\n        api_key = user_input.get(CONF_API_KEY)\n        st_entry_unique_id = user_input.get(CONF_ST_ENTRY_UNIQUE_ID)\n        if api_key and st_entry_unique_id:\n            return self._show_form(errors=\"only_key_or_st\", step_id=SOURCE_RECONFIGURE)\n\n        self._st_entry_unique_id = None\n        if st_entry_unique_id:\n            if not (api_key := get_smartthings_api_key(self.hass, st_entry_unique_id)):\n                return self._show_form(\n                    errors=\"st_api_key_fail\", step_id=SOURCE_RECONFIGURE\n                )\n            self._st_entry_unique_id = st_entry_unique_id\n        else:\n            api_key = api_key or entry.data.get(CONF_API_KEY)\n\n        self._host = ip_address\n        self._api_key = api_key\n\n        result = await self._try_connect(\n            port=entry.data.get(CONF_PORT),\n            token=entry.data.get(CONF_TOKEN),\n            skip_info=True,\n        )\n        return self._manage_reconfigure(result)\n\n    async def _manage_result(self, result: str, is_user_step=False) -> ConfigFlowResult:\n        \"\"\"Manage the previous result.\"\"\"\n\n        if result != RESULT_SUCCESS:\n            self._error = result\n            if result == RESULT_ST_DEVICE_NOT_FOUND:\n                return await self.async_step_stdeviceid()\n            if is_user_step:\n                return self._show_form()\n            return await self.async_step_user()\n\n        if ATTR_DEVICE_ID in self._device_info:\n            unique_id = self._device_info[ATTR_DEVICE_ID]\n        else:\n            mac = self._device_info.get(ATTR_DEVICE_MAC)\n            unique_id = mac or self._host  # as last option we use host as unique id\n\n        await self.async_set_unique_id(unique_id)\n        self._abort_if_unique_id_configured()\n\n        return self._save_entry()\n\n    @callback\n    def _manage_reconfigure(self, result: str) -> ConfigFlowResult:\n        \"\"\"Manage the reconfigure result.\"\"\"\n\n        if result != RESULT_SUCCESS:\n            self._error = result\n            return self._show_form(step_id=SOURCE_RECONFIGURE)\n\n        entry = self._get_reconfigure_entry()\n        updates = {\n            CONF_HOST: self._host,\n            CONF_PORT: self._tv_info.ws_port,\n        }\n        if self._token:\n            updates[CONF_TOKEN] = self._token\n\n        if self._api_key:\n            updates[CONF_API_KEY] = self._api_key\n            if CONF_ST_ENTRY_UNIQUE_ID in entry.data or self._st_entry_unique_id:\n                updates[CONF_ST_ENTRY_UNIQUE_ID] = self._st_entry_unique_id\n\n        return self.async_update_reload_and_abort(\n            entry, data_updates=updates, reload_even_if_entry_is_unchanged=False\n        )\n\n    @callback\n    def _save_entry(self) -> ConfigFlowResult:\n        \"\"\"Generate new entry.\"\"\"\n        data = {\n            CONF_HOST: self._host,\n            CONF_NAME: self._name,\n            CONF_PORT: self._tv_info.ws_port,\n            CONF_WS_NAME: self._ws_name,\n        }\n        if self._token:\n            data[CONF_TOKEN] = self._token\n\n        for key, attr in {\n            CONF_ID: ATTR_DEVICE_ID,\n            CONF_DEVICE_NAME: ATTR_DEVICE_NAME,\n            CONF_DEVICE_MODEL: ATTR_DEVICE_MODEL,\n            CONF_DEVICE_OS: ATTR_DEVICE_OS,\n            CONF_MAC: ATTR_DEVICE_MAC,\n        }.items():\n            if attr in self._device_info:\n                data[key] = self._device_info[attr]\n\n        title = self._name\n        if self._api_key and self._device_id:\n            data[CONF_API_KEY] = self._api_key\n            data[CONF_DEVICE_ID] = self._device_id\n            if self._st_entry_unique_id:\n                data[CONF_ST_ENTRY_UNIQUE_ID] = self._st_entry_unique_id\n            title += \" (SmartThings)\"\n\n        options = None\n        if self._ping_port:\n            options = {CONF_PING_PORT: self._ping_port}\n\n        _LOGGER.info(\"Configured new entity %s with host %s\", title, self._host)\n        return self.async_create_entry(title=title, data=data, options=options)\n\n    def _get_init_schema(self) -> vol.Schema:\n        \"\"\"Return the schema for initial configuration form.\"\"\"\n        data = self._user_data or {}\n        st_entries = get_smartthings_entries(self.hass)\n\n        init_schema = {\n            vol.Required(CONF_HOST, default=data.get(CONF_HOST, \"\")): str,\n            vol.Required(CONF_NAME, default=data.get(CONF_NAME, \"\")): str,\n            vol.Optional(\n                CONF_USE_HA_NAME, default=data.get(CONF_USE_HA_NAME, False)\n            ): bool,\n            vol.Optional(\n                CONF_API_KEY,\n                description={\"suggested_value\": data.get(CONF_API_KEY, \"\")},\n            ): str,\n        }\n\n        if st_entries:\n            st_unique_id = data.get(CONF_ST_ENTRY_UNIQUE_ID)\n            sugg_val = st_unique_id if st_unique_id in st_entries else None\n            init_schema.update(\n                {\n                    vol.Optional(\n                        CONF_ST_ENTRY_UNIQUE_ID,\n                        description={\"suggested_value\": sugg_val},\n                    ): SelectSelector(_dict_to_select(st_entries)),\n                }\n            )\n\n        return vol.Schema(init_schema)\n\n    def _get_reconfigure_schema(self) -> vol.Schema:\n        \"\"\"Return the schema for reconfiguration form.\"\"\"\n        entry = self._get_reconfigure_entry()\n        data = entry.data\n        st_entries = get_smartthings_entries(self.hass)\n\n        init_schema = {\n            vol.Required(CONF_HOST, default=data.get(CONF_HOST, \"\")): str,\n        }\n\n        if CONF_API_KEY in data and CONF_DEVICE_ID in data:\n            st_unique_id = data.get(CONF_ST_ENTRY_UNIQUE_ID)\n            use_st_key = st_entries is not None and st_unique_id in st_entries\n            sugg_val = data[CONF_API_KEY] if not use_st_key else \"\"\n            init_schema.update(\n                {\n                    vol.Optional(\n                        CONF_API_KEY, description={\"suggested_value\": sugg_val}\n                    ): str,\n                }\n            )\n\n            if st_entries:\n                sugg_val = st_unique_id if use_st_key else None\n                init_schema.update(\n                    {\n                        vol.Optional(\n                            CONF_ST_ENTRY_UNIQUE_ID,\n                            description={\"suggested_value\": sugg_val},\n                        ): SelectSelector(_dict_to_select(st_entries)),\n                    }\n                )\n\n        return vol.Schema(init_schema)\n\n    @callback\n    def _show_form(\n        self, errors: str | None = None, step_id=SOURCE_USER\n    ) -> ConfigFlowResult:\n        \"\"\"Show the form to the user.\"\"\"\n        base_err = errors or self._error\n        self._error = None\n\n        if step_id == \"stdevice\":\n            data_schema = self._st_devices_schema\n        elif step_id == \"stdeviceid\":\n            data_schema = vol.Schema({vol.Required(CONF_DEVICE_ID): str})\n        elif step_id == \"reconfigure\":\n            data_schema = self._get_reconfigure_schema()\n        else:\n            data_schema = self._get_init_schema()\n\n        return self.async_show_form(\n            step_id=step_id,\n            data_schema=data_schema,\n            errors={CONF_BASE: base_err} if base_err else None,\n        )\n\n    @staticmethod\n    @callback\n    def async_get_options_flow(config_entry) -> OptionsFlowHandler:\n        \"\"\"Get the options flow for this handler.\"\"\"\n        return OptionsFlowHandler(config_entry)\n\n\nclass OptionsFlowHandler(OptionsFlow):\n    \"\"\"Handle an option flow for Samsung TV Smart.\"\"\"\n\n    def __init__(self, config_entry: ConfigEntry) -> None:\n        \"\"\"Initialize options flow.\"\"\"\n        self._entry_id = config_entry.entry_id\n        self._adv_chk = False\n        self._std_options = config_entry.options.copy()\n        self._adv_options = {\n            key: values\n            for key, values in config_entry.options.items()\n            if key in ADVANCED_OPTIONS\n        }\n        self._sync_ent_opt = {\n            key: values\n            for key, values in config_entry.options.items()\n            if key in [CONF_SYNC_TURN_OFF, CONF_SYNC_TURN_ON]\n        }\n        self._app_list = self._std_options.get(CONF_APP_LIST)\n        self._channel_list = self._std_options.get(CONF_CHANNEL_LIST)\n        self._source_list = self._std_options.get(CONF_SOURCE_LIST)\n        api_key = config_entry.data.get(CONF_API_KEY)\n        st_dev = config_entry.data.get(CONF_DEVICE_ID)\n        self._use_st = api_key and st_dev\n\n    @callback\n    def _save_entry(self, data) -> ConfigFlowResult:\n        \"\"\"Save configuration options\"\"\"\n        data.update(self._adv_options)\n        data.update(self._sync_ent_opt)\n        entry_data = {k: v for k, v in data.items() if v is not None}\n        for key, value in entry_data.items():\n            if key in ENUM_OPTIONS:\n                entry_data[key] = int(value)\n        entry_data[CONF_APP_LIST] = self._app_list or {}\n        entry_data[CONF_CHANNEL_LIST] = self._channel_list or {}\n        entry_data[CONF_SOURCE_LIST] = self._source_list or {}\n\n        return self.async_create_entry(title=\"\", data=entry_data)\n\n    async def async_step_init(self, user_input=None) -> ConfigFlowResult:\n        \"\"\"Handle initial options flow.\"\"\"\n        if user_input is not None:\n            if self._adv_chk or user_input.pop(CONF_SHOW_ADV_OPT, False):\n                self._adv_chk = True\n                self._std_options = user_input\n                return await self.async_step_menu()\n            return self._save_entry(data=user_input)\n        return self._async_option_form()\n\n    @callback\n    def _async_option_form(self):\n        \"\"\"Return configuration form for options.\"\"\"\n        options = _validate_options(self._std_options)\n\n        opt_schema = {\n            vol.Required(\n                CONF_LOGO_OPTION,\n                default=options.get(CONF_LOGO_OPTION, str(LOGO_OPTION_DEFAULT.value)),\n            ): SelectSelector(_dict_to_select(LOGO_OPTIONS)),\n            vol.Required(\n                CONF_USE_LOCAL_LOGO,\n                default=options.get(CONF_USE_LOCAL_LOGO, True),\n            ): bool,\n        }\n\n        if not self._app_list:\n            opt_schema.update(\n                {\n                    vol.Required(\n                        CONF_APP_LOAD_METHOD,\n                        default=options.get(\n                            CONF_APP_LOAD_METHOD, str(AppLoadMethod.All.value)\n                        ),\n                    ): SelectSelector(_dict_to_select(APP_LOAD_METHODS)),\n                }\n            )\n\n        if self._use_st:\n            data_schema = vol.Schema(\n                {\n                    vol.Required(\n                        CONF_USE_ST_STATUS_INFO,\n                        default=options.get(CONF_USE_ST_STATUS_INFO, True),\n                    ): bool,\n                    vol.Required(\n                        CONF_USE_ST_CHANNEL_INFO,\n                        default=options.get(CONF_USE_ST_CHANNEL_INFO, True),\n                    ): bool,\n                    vol.Required(\n                        CONF_SHOW_CHANNEL_NR,\n                        default=options.get(CONF_SHOW_CHANNEL_NR, False),\n                    ): bool,\n                }\n            ).extend(opt_schema)\n            data_schema = data_schema.extend(\n                {\n                    vol.Required(\n                        CONF_POWER_ON_METHOD,\n                        default=options.get(\n                            CONF_POWER_ON_METHOD, str(PowerOnMethod.WOL.value)\n                        ),\n                    ): SelectSelector(_dict_to_select(POWER_ON_METHODS)),\n                }\n            )\n        else:\n            data_schema = vol.Schema(opt_schema)\n\n        if not self._adv_chk:\n            data_schema = data_schema.extend(\n                {vol.Required(CONF_SHOW_ADV_OPT, default=False): bool}\n            )\n\n        return self.async_show_form(step_id=\"init\", data_schema=data_schema)\n\n    async def async_step_menu(self, _=None):\n        \"\"\"Handle advanced options menu.\"\"\"\n        return self.async_show_menu(\n            step_id=\"menu\",\n            menu_options=[\n                \"source_list\",\n                \"app_list\",\n                \"channel_list\",\n                \"sync_ent\",\n                \"init\",\n                \"adv_opt\",\n                \"save_exit\",\n            ],\n        )\n\n    async def async_step_save_exit(self, _) -> ConfigFlowResult:\n        \"\"\"Handle save and exit flow.\"\"\"\n        return self._save_entry(data=self._std_options)\n\n    async def async_step_source_list(self, user_input=None):\n        \"\"\"Handle sources list flow.\"\"\"\n        errors: dict[str, str] | None = None\n        if user_input is not None:\n            valid_list = _validate_tv_list(user_input[CONF_SOURCE_LIST])\n            if valid_list is not None:\n                self._source_list = valid_list\n                return await self.async_step_menu()\n            errors = {CONF_BASE: \"invalid_tv_list\"}\n\n        data_schema = vol.Schema(\n            {\n                vol.Optional(\n                    CONF_SOURCE_LIST, default=self._source_list\n                ): ObjectSelector()\n            }\n        )\n        return self.async_show_form(\n            step_id=\"source_list\", data_schema=data_schema, errors=errors\n        )\n\n    async def async_step_app_list(self, user_input=None) -> ConfigFlowResult:\n        \"\"\"Handle apps list flow.\"\"\"\n        errors: dict[str, str] | None = None\n        if user_input is not None:\n            valid_list = _validate_tv_list(user_input[CONF_APP_LIST])\n            if valid_list is not None:\n                self._app_list = valid_list\n                return await self.async_step_menu()\n            errors = {CONF_BASE: \"invalid_tv_list\"}\n\n        data_schema = vol.Schema(\n            {vol.Optional(CONF_APP_LIST, default=self._app_list): ObjectSelector()}\n        )\n        return self.async_show_form(\n            step_id=\"app_list\", data_schema=data_schema, errors=errors\n        )\n\n    async def async_step_channel_list(self, user_input=None) -> ConfigFlowResult:\n        \"\"\"Handle channels list flow.\"\"\"\n        errors: dict[str, str] | None = None\n        if user_input is not None:\n            valid_list = _validate_tv_list(user_input[CONF_CHANNEL_LIST])\n            if valid_list is not None:\n                self._channel_list = valid_list\n                return await self.async_step_menu()\n            errors = {CONF_BASE: \"invalid_tv_list\"}\n\n        data_schema = vol.Schema(\n            {\n                vol.Optional(\n                    CONF_CHANNEL_LIST, default=self._channel_list\n                ): ObjectSelector()\n            }\n        )\n        return self.async_show_form(\n            step_id=\"channel_list\", data_schema=data_schema, errors=errors\n        )\n\n    async def async_step_sync_ent(self, user_input=None) -> ConfigFlowResult:\n        \"\"\"Handle syncronized entity flow.\"\"\"\n        if user_input is not None:\n            self._sync_ent_opt = user_input\n            return await self.async_step_menu()\n        return self._async_sync_ent_form()\n\n    @callback\n    def _async_sync_ent_form(self) -> ConfigFlowResult:\n        \"\"\"Return configuration form for syncronized entity.\"\"\"\n        select_entities = EntitySelectorConfig(\n            domain=_async_get_domains_service(self.hass, SERVICE_TURN_ON),\n            exclude_entities=_async_get_entry_entities(self.hass, self._entry_id),\n            multiple=True,\n        )\n        options = _validate_options(self._sync_ent_opt)\n\n        data_schema = vol.Schema(\n            {\n                vol.Optional(\n                    CONF_SYNC_TURN_OFF,\n                    description={\n                        \"suggested_value\": options.get(CONF_SYNC_TURN_OFF, [])\n                    },\n                ): EntitySelector(select_entities),\n                vol.Optional(\n                    CONF_SYNC_TURN_ON,\n                    description={\"suggested_value\": options.get(CONF_SYNC_TURN_ON, [])},\n                ): EntitySelector(select_entities),\n            }\n        )\n        return self.async_show_form(step_id=\"sync_ent\", data_schema=data_schema)\n\n    async def async_step_adv_opt(self, user_input=None) -> ConfigFlowResult:\n        \"\"\"Handle advanced options flow.\"\"\"\n        if user_input is not None:\n            self._adv_options = user_input\n            return await self.async_step_menu()\n        return self._async_adv_opt_form()\n\n    @callback\n    def _async_adv_opt_form(self) -> ConfigFlowResult:\n        \"\"\"Return configuration form for advanced options.\"\"\"\n        select_entities = EntitySelectorConfig(domain=BS_DOMAIN)\n        options = _validate_options(self._adv_options)\n\n        data_schema = vol.Schema(\n            {\n                vol.Required(\n                    CONF_APP_LAUNCH_METHOD,\n                    default=options.get(\n                        CONF_APP_LAUNCH_METHOD, str(AppLaunchMethod.Standard.value)\n                    ),\n                ): SelectSelector(_dict_to_select(APP_LAUNCH_METHODS)),\n                vol.Required(\n                    CONF_WOL_REPEAT,\n                    default=min(options.get(CONF_WOL_REPEAT, 1), MAX_WOL_REPEAT),\n                ): vol.All(vol.Coerce(int), vol.Clamp(min=1, max=MAX_WOL_REPEAT)),\n                vol.Required(\n                    CONF_PING_PORT, default=options.get(CONF_PING_PORT, 0)\n                ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=65535)),\n                vol.Optional(\n                    CONF_EXT_POWER_ENTITY,\n                    description={\n                        \"suggested_value\": options.get(CONF_EXT_POWER_ENTITY, \"\")\n                    },\n                ): EntitySelector(select_entities),\n                vol.Required(\n                    CONF_USE_MUTE_CHECK,\n                    default=options.get(CONF_USE_MUTE_CHECK, False),\n                ): bool,\n                vol.Required(\n                    CONF_DUMP_APPS,\n                    default=options.get(CONF_DUMP_APPS, False),\n                ): bool,\n                vol.Required(\n                    CONF_TOGGLE_ART_MODE,\n                    default=options.get(CONF_TOGGLE_ART_MODE, False),\n                ): bool,\n            }\n        )\n        return self.async_show_form(step_id=\"adv_opt\", data_schema=data_schema)\n\n\ndef _validate_options(options: dict) -> dict:\n    \"\"\"Validate options format\"\"\"\n    valid_options = {}\n    for opt_key, opt_val in options.items():\n        if opt_key in [CONF_SYNC_TURN_OFF, CONF_SYNC_TURN_ON]:\n            if not isinstance(opt_val, list):\n                continue\n        if opt_key in ENUM_OPTIONS:\n            valid_options[opt_key] = str(opt_val)\n        else:\n            valid_options[opt_key] = opt_val\n    return valid_options\n\n\ndef _validate_tv_list(input_list: dict[str, Any]) -> dict[str, str] | None:\n    \"\"\"Validate TV list from object selector.\"\"\"\n    valid_list = {}\n    for name_val, id_val in input_list.items():\n        if not id_val:\n            continue\n        if isinstance(id_val, Number):\n            id_val = str(id_val)\n        if not isinstance(id_val, str):\n            return None\n        valid_list[name_val] = id_val\n    return valid_list\n\n\ndef _dict_to_select(opt_dict: dict) -> SelectSelectorConfig:\n    \"\"\"Covert a dict to a SelectSelectorConfig.\"\"\"\n    return SelectSelectorConfig(\n        options=[SelectOptionDict(value=str(k), label=v) for k, v in opt_dict.items()],\n        mode=SelectSelectorMode.DROPDOWN,\n    )\n\n\ndef _async_get_domains_service(hass: HomeAssistant, service_name: str) -> list[str]:\n    \"\"\"Fetch list of domain that provide a specific service.\"\"\"\n    return [\n        domain\n        for domain, service in hass.services.async_services().items()\n        if service_name in service\n    ]\n\n\ndef _async_get_entry_entities(hass: HomeAssistant, entry_id: str) -> list[str]:\n    \"\"\"Get the entities related to current entry\"\"\"\n    return [\n        entry.entity_id\n        for entry in (er.async_entries_for_config_entry(er.async_get(hass), entry_id))\n    ]\n"
  },
  {
    "path": "custom_components/samsungtv_smart/const.py",
    "content": "\"\"\"Constants for the samsungtv_smart integration.\"\"\"\n\nfrom enum import Enum\n\n\nclass AppLoadMethod(Enum):\n    \"\"\"Valid application load methods.\"\"\"\n\n    All = 1\n    Default = 2\n    NotLoad = 3\n\n\nclass AppLaunchMethod(Enum):\n    \"\"\"Valid application launch methods.\"\"\"\n\n    Standard = 1\n    Remote = 2\n    Rest = 3\n\n\nclass PowerOnMethod(Enum):\n    \"\"\"Valid power on methods.\"\"\"\n\n    WOL = 1\n    SmartThings = 2\n\n\nDOMAIN = \"samsungtv_smart\"\n\nMIN_HA_MAJ_VER = 2025\nMIN_HA_MIN_VER = 6\n__min_ha_version__ = f\"{MIN_HA_MAJ_VER}.{MIN_HA_MIN_VER}.0\"\n\nDATA_CFG = \"cfg\"\nDATA_CFG_YAML = \"cfg_yaml\"\nDATA_OPTIONS = \"options\"\nLOCAL_LOGO_PATH = \"local_logo_path\"\nWS_PREFIX = \"[Home Assistant]\"\n\nATTR_DEVICE_MAC = \"device_mac\"\nATTR_DEVICE_MODEL = \"device_model\"\nATTR_DEVICE_NAME = \"device_name\"\nATTR_DEVICE_OS = \"device_os\"\n\nCONF_APP_LAUNCH_METHOD = \"app_launch_method\"\nCONF_APP_LIST = \"app_list\"\nCONF_APP_LOAD_METHOD = \"app_load_method\"\nCONF_CHANNEL_LIST = \"channel_list\"\nCONF_DEVICE_MODEL = \"device_model\"\nCONF_DEVICE_NAME = \"device_name\"\nCONF_DEVICE_OS = \"device_os\"\nCONF_DUMP_APPS = \"dump_apps\"\nCONF_EXT_POWER_ENTITY = \"ext_power_entity\"\nCONF_LOAD_ALL_APPS = \"load_all_apps\"\nCONF_LOGO_OPTION = \"logo_option\"\nCONF_PING_PORT = \"ping_port\"\nCONF_POWER_ON_METHOD = \"power_on_method\"\nCONF_SHOW_CHANNEL_NR = \"show_channel_number\"\nCONF_SOURCE_LIST = \"source_list\"\nCONF_SYNC_TURN_OFF = \"sync_turn_off\"\nCONF_SYNC_TURN_ON = \"sync_turn_on\"\nCONF_TOGGLE_ART_MODE = \"toggle_art_mode\"\nCONF_USE_LOCAL_LOGO = \"use_local_logo\"\nCONF_USE_MUTE_CHECK = \"use_mute_check\"\nCONF_USE_ST_CHANNEL_INFO = \"use_st_channel_info\"\nCONF_USE_ST_STATUS_INFO = \"use_st_status_info\"\nCONF_WOL_REPEAT = \"wol_repeat\"\nCONF_WS_NAME = \"ws_name\"\n\n# for SmartThings integration api key usage\nCONF_ST_ENTRY_UNIQUE_ID = \"st_entry_unique_id\"\nCONF_USE_ST_INT_API_KEY = \"use_st_int_api_key\"  # obsolete used for migration\n\n# obsolete\nCONF_UPDATE_METHOD = \"update_method\"\nCONF_UPDATE_CUSTOM_PING_URL = \"update_custom_ping_url\"\nCONF_SCAN_APP_HTTP = \"scan_app_http\"\n\nDEFAULT_APP = \"TV/HDMI\"\nDEFAULT_PORT = 8001\nDEFAULT_SOURCE_LIST = {\"TV\": \"KEY_TV\", \"HDMI\": \"KEY_HDMI\"}\nDEFAULT_TIMEOUT = 6\n\nMAX_WOL_REPEAT = 5\n\nRESULT_NOT_SUCCESSFUL = \"not_successful\"\nRESULT_NOT_SUPPORTED = \"not_supported\"\nRESULT_ST_DEVICE_USED = \"st_device_used\"\nRESULT_ST_DEVICE_NOT_FOUND = \"st_device_not_found\"\nRESULT_ST_MULTI_DEVICES = \"st_multiple_device\"\nRESULT_SUCCESS = \"success\"\nRESULT_WRONG_APIKEY = \"wrong_api_key\"\n\nSERVICE_SELECT_PICTURE_MODE = \"select_picture_mode\"\nSERVICE_SET_ART_MODE = \"set_art_mode\"\n\nSIGNAL_CONFIG_ENTITY = f\"{DOMAIN}_config\"\n\nSTD_APP_LIST = {\n    \"org.tizen.browser\": {\n        \"st_app_id\": \"\",\n        \"logo\": \"tizenbrowser.png\",\n    },  # Internet\n    \"11101200001\": {\n        \"st_app_id\": \"RN1MCdNq8t.Netflix\",\n        \"logo\": \"netflix.png\",\n    },  # Netflix\n    \"3201907018807\": {\n        \"st_app_id\": \"org.tizen.netflix-app\",\n        \"logo\": \"netflix.png\",\n    },  # Netflix (New)\n    \"111299001912\": {\n        \"st_app_id\": \"9Ur5IzDKqV.TizenYouTube\",\n        \"logo\": \"youtube.png\",\n    },  # YouTube\n    \"3201512006785\": {\n        \"st_app_id\": \"org.tizen.ignition\",\n        \"logo\": \"primevideo.png\",\n    },  # Prime Video\n    # \"3201512006785\": {\n    #     \"st_app_id\": \"evKhCgZelL.AmazonIgnitionLauncher2\",\n    #     \"logo\": \"\",\n    # },  # Prime Video\n    \"3201901017640\": {\n        \"st_app_id\": \"MCmYXNxgcu.DisneyPlus\",\n        \"logo\": \"disneyplus.png\",\n    },  # Disney+\n    \"3202110025305\": {\n        \"st_app_id\": \"rJyOSqC6Up.PPlusIntl\",\n        \"logo\": \"paramountplus.png\",\n    },  # Paramount+\n    \"11091000000\": {\n        \"st_app_id\": \"4ovn894vo9.Facebook\",\n        \"logo\": \"facebook.png\",\n    },  # Facebook\n    \"3201806016390\": {\n        \"st_app_id\": \"yu1NM3vHsU.DAZN\",\n        \"logo\": \"dazn.png\",\n    },  # Dazn\n    \"3201601007250\": {\n        \"st_app_id\": \"QizQxC7CUf.PlayMovies\",\n        \"logo\": \"\",\n    },  # Google Play\n    \"3201606009684\": {\n        \"st_app_id\": \"rJeHak5zRg.Spotify\",\n        \"logo\": \"spotify.png\",\n    },  # Spotify\n    \"3201512006963\": {\n        \"st_app_id\": \"kIciSQlYEM.plex\",\n        \"logo\": \"\",\n    },  # Plex\n}\n"
  },
  {
    "path": "custom_components/samsungtv_smart/diagnostics.py",
    "content": "\"\"\"Diagnostics support for Samsung TV Smart.\"\"\"\n\nfrom __future__ import annotations\n\nfrom homeassistant.components.diagnostics import REDACTED, async_redact_data\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import CONF_API_KEY, CONF_ID, CONF_MAC, CONF_TOKEN\nfrom homeassistant.core import HomeAssistant, callback\nfrom homeassistant.helpers import device_registry as dr, entity_registry as er\n\nfrom .const import DOMAIN\n\nTO_REDACT = {CONF_API_KEY, CONF_MAC, CONF_TOKEN}\n\n\nasync def async_get_config_entry_diagnostics(\n    hass: HomeAssistant, entry: ConfigEntry\n) -> dict:\n    \"\"\"Return diagnostics for a config entry.\"\"\"\n    diag_data = {\"entry\": async_redact_data(entry.as_dict(), TO_REDACT)}\n\n    yaml_data = hass.data[DOMAIN].get(entry.unique_id, {})\n    if yaml_data:\n        diag_data[\"config_data\"] = async_redact_data(yaml_data, TO_REDACT)\n\n    device_id = entry.data.get(CONF_ID, entry.entry_id)\n    hass_data = _async_device_ha_info(hass, device_id)\n    if hass_data:\n        diag_data[\"device\"] = hass_data\n\n    return diag_data\n\n\n@callback\ndef _async_device_ha_info(hass: HomeAssistant, device_id: str) -> dict | None:\n    \"\"\"Gather information how this TV device is represented in Home Assistant.\"\"\"\n\n    device_registry = dr.async_get(hass)\n    entity_registry = er.async_get(hass)\n    hass_device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)})\n    if not hass_device:\n        return None\n\n    data = {\n        \"name\": hass_device.name,\n        \"name_by_user\": hass_device.name_by_user,\n        \"model\": hass_device.model,\n        \"manufacturer\": hass_device.manufacturer,\n        \"sw_version\": hass_device.sw_version,\n        \"disabled\": hass_device.disabled,\n        \"disabled_by\": hass_device.disabled_by,\n        \"entities\": {},\n    }\n\n    hass_entities = er.async_entries_for_device(\n        entity_registry,\n        device_id=hass_device.id,\n        include_disabled_entities=True,\n    )\n\n    for entity_entry in hass_entities:\n        if entity_entry.platform != DOMAIN:\n            continue\n        state = hass.states.get(entity_entry.entity_id)\n        state_dict = None\n        if state:\n            state_dict = dict(state.as_dict())\n            # The entity_id is already provided at root level.\n            state_dict.pop(\"entity_id\", None)\n            # The context doesn't provide useful information in this case.\n            state_dict.pop(\"context\", None)\n            # Redact the `entity_picture` attribute as it contains a token.\n            if \"entity_picture\" in state_dict[\"attributes\"]:\n                state_dict[\"attributes\"] = {\n                    **state_dict[\"attributes\"],\n                    \"entity_picture\": REDACTED,\n                }\n\n        data[\"entities\"][entity_entry.entity_id] = {\n            \"name\": entity_entry.name,\n            \"original_name\": entity_entry.original_name,\n            \"disabled\": entity_entry.disabled,\n            \"disabled_by\": entity_entry.disabled_by,\n            \"entity_category\": entity_entry.entity_category,\n            \"device_class\": entity_entry.device_class,\n            \"original_device_class\": entity_entry.original_device_class,\n            \"icon\": entity_entry.icon,\n            \"original_icon\": entity_entry.original_icon,\n            \"unit_of_measurement\": entity_entry.unit_of_measurement,\n            \"state\": state_dict,\n        }\n\n    return data\n"
  },
  {
    "path": "custom_components/samsungtv_smart/entity.py",
    "content": "\"\"\"Base SamsungTV Entity.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom homeassistant.const import (\n    ATTR_CONNECTIONS,\n    ATTR_IDENTIFIERS,\n    ATTR_SW_VERSION,\n    CONF_HOST,\n    CONF_ID,\n    CONF_MAC,\n    CONF_NAME,\n)\nfrom homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC\nfrom homeassistant.helpers.entity import DeviceInfo, Entity\n\nfrom .const import CONF_DEVICE_MODEL, CONF_DEVICE_NAME, CONF_DEVICE_OS, DOMAIN\n\n\nclass SamsungTVEntity(Entity):\n    \"\"\"Defines a base SamsungTV entity.\"\"\"\n\n    _attr_has_entity_name = True\n\n    def __init__(self, config: dict[str, Any], entry_id: str) -> None:\n        \"\"\"Initialize the class.\"\"\"\n        self._name = config.get(CONF_NAME, config[CONF_HOST])\n        self._mac = config.get(CONF_MAC)\n        self._attr_unique_id = config.get(CONF_ID, entry_id)\n\n        model = config.get(CONF_DEVICE_MODEL, \"Samsung TV\")\n        if dev_name := config.get(CONF_DEVICE_NAME):\n            model = f\"{model} ({dev_name})\"\n\n        self._attr_device_info = DeviceInfo(\n            manufacturer=\"Samsung Electronics\",\n            model=model,\n            name=self._name,\n        )\n        if self.unique_id:\n            self._attr_device_info[ATTR_IDENTIFIERS] = {(DOMAIN, self.unique_id)}\n        if dev_os := config.get(CONF_DEVICE_OS):\n            self._attr_device_info[ATTR_SW_VERSION] = dev_os\n        if self._mac:\n            self._attr_device_info[ATTR_CONNECTIONS] = {\n                (CONNECTION_NETWORK_MAC, self._mac)\n            }\n"
  },
  {
    "path": "custom_components/samsungtv_smart/logo.py",
    "content": "\"\"\"Logo implementation for SamsungTV Smart.\"\"\"\n\nimport asyncio\nfrom datetime import datetime, timedelta, timezone\nfrom enum import Enum\nimport json\nimport logging\nimport os\nfrom pathlib import Path\nimport re\nimport traceback\nfrom typing import Optional\n\nimport aiofiles\nfrom aiofiles import os as aiopath\nimport aiohttp\n\nfrom .const import DOMAIN\n\n\n# Logo feature constants\nclass LogoOption(Enum):\n    \"\"\"List of posible logo options.\"\"\"\n\n    Disabled = 1\n    WhiteColor = 2\n    BlueColor = 3\n    BlueWhite = 4\n    DarkWhite = 5\n    TransparentColor = 6\n    TransparentWhite = 7\n\n\nCUSTOM_IMAGE_BASE_URL = f\"/api/{DOMAIN}/custom\"\nSTATIC_IMAGE_BASE_URL = f\"/api/{DOMAIN}/static\"\nCHAR_REPLACE = {\" \": \"\", \"+\": \"plus\", \"_\": \"\", \".\": \"\", \":\": \"\"}\n\nLOGO_OPTIONS_MAPPING = {\n    LogoOption.Disabled: \"none\",\n    LogoOption.WhiteColor: \"fff-color\",\n    LogoOption.BlueColor: \"05a9f4-color\",\n    LogoOption.BlueWhite: \"05a9f4-white\",\n    LogoOption.DarkWhite: \"282c34-white\",\n    LogoOption.TransparentColor: \"transparent-color\",\n    LogoOption.TransparentWhite: \"transparent-white\",\n}\nLOGO_OPTION_DEFAULT = LogoOption.WhiteColor\nLOGO_BASE_URL = \"https://jaruba.github.io/channel-logos/\"\nLOGO_FILE = \"logo_paths.json\"\nLOGO_FILE_DOWNLOAD = \"logo_paths_download.json\"\nLOGO_FILE_DAYS_BEFORE_UPDATE = 1\nLOGO_MIN_SCORE_REQUIRED = 80\nLOGO_MEDIATITLE_KEYWORD_REMOVAL = [\"HDTV\", \"HD\"]\nLOGO_MAX_PATHS = 30000\nLOGO_NO_MATCH = \"NO_MATCH\"\nMAX_LOGO_CACHE = 200\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass LocalImageUrl:\n    \"\"\"Class to manage the local image url.\"\"\"\n\n    def __init__(self, custom_logo_path=None):\n        \"\"\"Initialise the local image url class.\"\"\"\n        self._custom_logo_path = custom_logo_path\n        self._local_image_url = None\n        self._last_media_title = None\n\n    def get_image_url(self, media_title, local_logo_file=None):\n        \"\"\"Check local image is present.\"\"\"\n        if not media_title and not local_logo_file:\n            return None\n\n        cf_local_logo_file = None\n        if local_logo_file:\n            cf_local_logo_file = local_logo_file.casefold()\n            if cf_local_logo_file == self._last_media_title:\n                return self._local_image_url\n        if media_title == self._last_media_title:\n            return self._local_image_url\n\n        self._last_media_title = cf_local_logo_file or media_title\n        self._local_image_url = None\n\n        media_logo_file = media_title\n        for searcher, replacer in CHAR_REPLACE.items():\n            media_logo_file = media_logo_file.replace(searcher, replacer)\n        media_logo_file += \".png\"\n\n        if self._custom_logo_path:\n            for logo_file in Path(self._custom_logo_path).iterdir():\n                if logo_file.name.casefold() == media_logo_file.casefold():\n                    self._local_image_url = f\"{CUSTOM_IMAGE_BASE_URL}/{logo_file.name}\"\n                    self._last_media_title = media_title\n                    break\n\n        if not self._local_image_url and local_logo_file:\n            dir_path = Path(__file__).parent / \"static\"\n            for logo_file in Path(dir_path).iterdir():\n                if logo_file.name.casefold() == local_logo_file.casefold():\n                    self._local_image_url = f\"{STATIC_IMAGE_BASE_URL}/{logo_file.name}\"\n                    break\n\n        return self._local_image_url\n\n\nclass Logo:\n    \"\"\"\n    Class that fetches logos for Samsung TV Tizen.\n    Works with https://github.com/jaruba/channel-logos.\n    \"\"\"\n\n    def __init__(\n        self,\n        logo_option: LogoOption,\n        logo_file_download: str = None,\n        session: Optional[aiohttp.ClientSession] = None,\n    ):\n        self._media_image_base_url = None\n        self._logo_option = None\n        self.set_logo_color(logo_option)\n        if session:\n            self._session = session\n        else:\n            self._session = aiohttp.ClientSession()\n\n        self._images_paths = None\n        self._logo_cache = {}\n        self._last_check = None\n\n        app_path = os.path.dirname(os.path.realpath(__file__))\n        self._logo_file_path = os.path.join(app_path, LOGO_FILE)\n        self._logo_file_download_path = logo_file_download or os.path.join(\n            app_path, LOGO_FILE_DOWNLOAD\n        )\n\n    def set_logo_color(self, logo_type: LogoOption):\n        \"\"\"Sets the logo color option and image base url if not already set to this option\"\"\"\n        logo_option = LOGO_OPTIONS_MAPPING[logo_type]\n        if self._logo_option and self._logo_option == logo_option:\n            return\n\n        _LOGGER.debug(\"Setting logo option to %s\", logo_option)\n        self._logo_option = logo_option\n\n        if logo_type == LogoOption.Disabled:\n            self._media_image_base_url = None\n        else:\n            self._media_image_base_url = f\"{LOGO_BASE_URL}export/{self._logo_option}\"\n\n    def check_requested(self):\n        \"\"\"Check if a new file update is requested.\"\"\"\n        if self._media_image_base_url is None:\n            return False\n\n        check_time = datetime.now(timezone.utc).astimezone()\n        if self._last_check is not None and self._last_check > check_time - timedelta(\n            days=LOGO_FILE_DAYS_BEFORE_UPDATE\n        ):\n            return False\n\n        return True\n\n    async def _async_ensure_latest_path_file(self):\n        \"\"\"Does check if logo paths file exists and if it does - is it out of date or not.\"\"\"\n        if not self.check_requested():\n            return\n\n        check_time = datetime.now(timezone.utc).astimezone()\n        update_file = not await aiopath.path.isfile(self._logo_file_download_path)\n        if not update_file:\n            file_date = datetime.fromtimestamp(\n                await aiopath.path.getmtime(self._logo_file_download_path), timezone.utc\n            ).astimezone()\n            if file_date > check_time - timedelta(days=LOGO_FILE_DAYS_BEFORE_UPDATE):\n                self._last_check = file_date\n                return\n\n            try:\n                async with self._session.head(\n                    LOGO_BASE_URL + \"logo_paths.json\"\n                ) as response:\n                    url_date = datetime.strptime(\n                        response.headers.get(\"Last-Modified\"),\n                        \"%a, %d %b %Y %X %Z\",\n                    ).astimezone()\n                    update_file = url_date > file_date\n            except (aiohttp.ClientError, asyncio.TimeoutError):\n                _LOGGER.warning(\n                    \"Not able to check for latest paths file for logos from %s%s. \"\n                    \"Check if the URL is accessible from this machine\",\n                    LOGO_BASE_URL,\n                    \"logo_paths.json\",\n                )\n\n        self._last_check = check_time\n        if update_file:\n            if await self._download_latest_path_file():\n                await self._read_path_file(True)\n\n    async def _download_latest_path_file(self):\n        \"\"\"Download the last available logo file.\"\"\"\n        try:\n            async with self._session.get(LOGO_BASE_URL + \"logo_paths.json\") as response:\n                response = (await response.read()).decode(\"utf-8\")\n                if response:\n                    async with aiofiles.open(\n                        self._logo_file_download_path, mode=\"w+\", encoding=\"utf-8\"\n                    ) as paths_file:\n                        await paths_file.write(response)\n\n            return True\n\n        except (aiohttp.ClientError, asyncio.TimeoutError):\n            _LOGGER.warning(\n                \"Not able to download latest paths file for logos from %s%s. \"\n                \"Check if the URL is accessible from this machine.\",\n                LOGO_BASE_URL,\n                \"logo_paths.json\",\n            )\n        except PermissionError:\n            _LOGGER.warning(\n                \"No permission while trying to write the downloaded paths file to %s. \"\n                \"Please check file writing permissions.\",\n                self._logo_file_download_path,\n            )\n        except OSError:\n            _LOGGER.warning(\n                \"Not able to write the downloaded paths file to %s. \"\n                \"Disk might be full or another OS error occurred\",\n                self._logo_file_download_path,\n            )\n            _LOGGER.warning(traceback.print_exc())\n\n        return False\n\n    async def _read_path_file(self, force_read=False):\n        \"\"\"Read the logo path file and store result locally.\"\"\"\n        if self._images_paths and not force_read:\n            return\n\n        logo_file = None\n        if await aiopath.path.isfile(self._logo_file_download_path):\n            logo_file = self._logo_file_download_path\n        elif await aiopath.path.isfile(self._logo_file_path):\n            logo_file = self._logo_file_path\n\n        if not logo_file:\n            _LOGGER.warning(\n                \"Not able to search for a logo. Logo paths file might be missing at %s or %s\",\n                self._logo_file_download_path,\n                self._logo_file_path,\n            )\n            return\n\n        try:\n            async with aiofiles.open(logo_file, \"r\") as f:\n                image_paths = json.loads(await f.read())\n        except Exception as exc:  # pylint: disable=broad-except\n            _LOGGER.warning(\"Failed to read logo paths file %s: %s\", logo_file, exc)\n            return\n\n        if image_paths:\n            self._logo_cache = {}\n            self._images_paths = image_paths\n\n    def _add_to_cache(self, media_title, logo_path=LOGO_NO_MATCH):\n        \"\"\"Add a new item to the logo cache.\"\"\"\n        if len(self._logo_cache) > MAX_LOGO_CACHE:\n            self._logo_cache.popitem()\n        self._logo_cache[media_title] = logo_path\n\n    async def async_find_match(self, media_title):\n        \"\"\"Finds a match in the logo_paths file for a given media_title\"\"\"\n        if self._media_image_base_url is None:\n            _LOGGER.debug(\n                \"Media image base url was not set! Not able to find a matching logo\"\n            )\n            return None\n\n        if media_title is None:\n            _LOGGER.warning(\n                \"No media title right now! Not able to find a matching logo\"\n            )\n            return None\n\n        _LOGGER.debug(\"Matching media title for %s\", media_title)\n        await self._async_ensure_latest_path_file()\n\n        # remove string between parenthesis ()\n        removal = re.finditer(r\"\\((.*?)\\)\", media_title)\n        for match in removal:\n            media_title = media_title.replace(match.group(), \"\")\n\n        # remove specific strings\n        for word in LOGO_MEDIATITLE_KEYWORD_REMOVAL:\n            media_title = media_title.lower().replace(word.lower(), \"\")\n\n        # remove leading and trailing spaces\n        media_title = media_title.lower().strip()\n\n        # check if log is in the cache\n        cached_logo = self._logo_cache.get(media_title)\n        if cached_logo:\n            if cached_logo == LOGO_NO_MATCH:\n                return None\n            return self._media_image_base_url + cached_logo\n\n        # search best matching logo\n        await self._read_path_file()\n        if not self._images_paths:\n            return None\n\n        best_match = {\"ratio\": None, \"title\": None, \"path\": None}\n        paths_checked = 0\n        for image_path in iter(self._images_paths.items()):\n            if paths_checked > LOGO_MAX_PATHS:\n                _LOGGER.warning(\n                    \"Exceeded maximum amount of paths (%d) while searching for a match. Halting the search\",\n                    LOGO_MAX_PATHS,\n                )\n                break\n            ratio = _levenshtein_ratio(media_title, image_path[0].lower())\n            if best_match[\"ratio\"] is None or ratio > best_match[\"ratio\"]:\n                best_match = {\n                    \"ratio\": ratio,\n                    \"title\": image_path[0],\n                    \"path\": image_path[1],\n                }\n            if best_match[\"ratio\"] == 1:\n                break\n            paths_checked += 1\n\n        best_ratio = best_match[\"ratio\"] or 0.0\n        best_path = best_match[\"path\"] or \"\"\n        if best_ratio >= LOGO_MIN_SCORE_REQUIRED / 100 and best_path:\n            found_logo = self._media_image_base_url + best_path\n            _LOGGER.debug(\n                \"Match found for %s: %s (%f) %s\",\n                media_title,\n                best_match[\"title\"],\n                best_ratio,\n                found_logo,\n            )\n            self._add_to_cache(media_title, best_path)\n            return found_logo\n\n        _LOGGER.debug(\n            \"No match found for %s: best candidate was %s (%f) %s\",\n            media_title,\n            best_match[\"title\"],\n            best_ratio,\n            self._media_image_base_url + best_path,\n        )\n        self._add_to_cache(media_title)\n        return None\n\n\ndef _levenshtein_ratio(s: str, t: str):\n    \"\"\"Calculate match ratio between 2 strings.\"\"\"\n    if not (s and t):\n        return 0.0\n\n    rows = len(s) + 1\n    cols = len(t) + 1\n    distance = [[0 for _ in range(cols)] for _ in range(rows)]\n\n    for i in range(1, rows):\n        for k in range(1, cols):\n            distance[i][0] = i\n            distance[0][k] = k\n\n    for col in range(1, cols):\n        for row in range(1, rows):\n            if s[row - 1] == t[col - 1]:\n                cost = 0\n            else:\n                cost = 2\n            distance[row][col] = min(\n                distance[row - 1][col] + 1,\n                distance[row][col - 1] + 1,\n                distance[row - 1][col - 1] + cost,\n            )\n\n    ratio = ((len(s) + len(t)) - distance[rows - 1][cols - 1]) / (len(s) + len(t))\n    return ratio\n"
  },
  {
    "path": "custom_components/samsungtv_smart/logo_paths.json",
    "content": "{\"fuji tv\":\"/yS5UJjsSdZXML0YikWTYYHLPKhQ.png\",\"abc\":\"/kMvN5R8g6L0SY5r9YZw9foYGQr0.png\",\"bbc three\":\"/ex369Frq6w0PaQsVofp21C6tbuC.png\",\"bbc one\":\"/mVn7xESaTNmjBUyUtGNvDQd3CT1.png\",\"bell tv\":\"/5hksRDWDoqYYkq0q7KWk4MkMCoZ.png\",\"nbc\":\"/sGeMxrk8fWDYnVMFut8IXnuIy0R.png\",\"māori television\":\"/bwO92lNZstiQHtM7CSD7YNPGYM.png\",\"itv\":\"/bT7I2LdsCyEtKKNRZ1p15Emz14B.png\",\"discovery health channel\":\"/yiBepnHS6gdzlT6ZIehYnNl0nEG.png\",\"cctv\":\"/ufmYwN934igUGuPaorjcfsg57IU.png\",\"htb\":\"/7wuTb7iMz5yLoJ8hIyBnUxqR3mP.png\",\"nickelodeon\":\"/ikZXxg6GnwpzqiZbRPhJGaZapqB.png\",\"pbs\":\"/d4OH7tMO4ece61s4j7mJWqQejv.png\",\"cbbc\":\"/9O6hVu6gePt7097QdqKxxSF9Suh.png\",\"cbs\":\"/nm8d7P7MJNiBLdgIzUK0gkuEA4r.png\",\"mtv2\":\"/6o7HFq6Sm6mOt5Kh5iL6VzqJ4No.png\",\"fox\":\"/mGIwo5uKaPMK4sRNwSwRl9nmRtd.png\",\"abs-cbn\":\"/kdcvqvutGKUsIkeSVGl8pQMkc3k.png\",\"the wb\":\"/9GlDHjQj9c2dkfARCR3zlH87R66.png\",\"discovery kids\":\"/8woBOtitimA6diobq7sxCIwj37G.png\",\"cbc television\":\"/cw5WW6cc9UANam4A6o1BDua9njN.png\",\"bet\":\"/pejxmP1m6GiUZj01jhDN4a2tHKq.png\",\"televisión nacional de chile\":\"/4aSX239LVbBu6LPqlUBdNMz0W3B.png\",\"channel 4\":\"/hbifXPpM55B1fL5wPo7t72vzN78.png\",\"univision\":\"/72FxNFjskNZMIKXoYrqkmmBROzM.png\",\"espn\":\"/giGGjdUouGoQki7LAIvhAzXdjbN.png\",\"usa network\":\"/g1e0H0Ka97IG5SyInMXdJkHGKiH.png\",\"zdf\":\"/sfZaVx3svlzIoNoerdgb840TPD9.png\",\"mtv\":\"/e9GMyvaguUc36ktS7iSFYP0WLKa.png\",\"lifetime\":\"/kEeaVLcJ6L6jq3v5YlPcjQs9igm.png\",\"nick jr.\":\"/zmXCDvQt7AUf5ci1ldjFfccsmJc.png\",\"atv\":\"/loMzcvCNgox1LdFwpEQD8Q5vvdA.png\",\"msnbc\":\"/eV4rShI9xT9tzziSFFqGvVbNtza.png\",\"network ten\":\"/cUq4tWKFvubij22k2v3gGu0cniX.png\",\"irib tv5\":\"/r5hZ043QIKBhvIGVOcwN5FVn5Me.png\",\"upn\":\"/333LtWX9Z7H9uRrNcCl1JcTvdpR.png\",\"tnt\":\"/krzxf46PwPXxUtjnn3g4eqrcyIu.png\",\"tv5\":\"/gVinXXdpdbLHuXTUUfx4R9PyuPB.png\",\"national geographic\":\"/q9rPBG1rHbUjII1Qn98VG2v7cFa.png\",\"disney xd\":\"/nKM9EnV7jTpt3MKRbhBusJ03lAY.png\",\"fox news channel\":\"/zYjYOy304S4JPXWncHS6D5K0mdi.png\",\"ytv\":\"/cIMyE9cw1W4kMFGxmC17HKTnVz9.png\",\"comedy central\":\"/6ooPjtXufjsoskdJqj6pxuvHEno.png\",\"tvb jade\":\"/8QQTIU1Rd0Stw2jmlSi4emv6PkK.png\",\"hbo\":\"/tuomPhY2UtuPTqqFnKMVHvSb724.png\",\"gma network\":\"/ftB1h0NyMIeu1H1AtZ8BXtsYIiA.png\",\"the comedy network\":\"/zWANL77d5GiGouIaL5o8a2hErRv.png\",\"telecinco\":\"/3q9Kob7joLOHrsJPXQ9clhsF4az.png\",\"disney channel\":\"/dZvpMwi5e6FFgdJlP08yHb1JlJG.png\",\"spike\":\"/n2GcqhLEJF1elvp9v5XylSByM5J.png\",\"cartoon network\":\"/nDFWFbAHEZ8Towq7fsVCgv4U245.png\",\"ntv\":\"/9XFdqimbyRfbFHulEg1LEpKWE8z.png\",\"televisa\":\"/3vFK8I1mW11dBfUcCopWd5rWDBH.png\",\"cnn\":\"/2EIKV6SomKx8L52HoSViKyJO87m.png\",\"rede globo\":\"/qFNe75EkUaIdNlTqadArD00a62m.png\",\"outtv\":\"/rJI8nxnuiSB4O0roWpAAB7BEcqv.png\",\"logo\":\"/ztSNqnJ8W1GOYfa3525xFBDb3NY.png\",\"game show network\":\"/er3Bx1TMCviOUvhIXyOtFhL9f8w.png\",\"discovery\":\"/tmttRFo2OiXQD0EHMxxlw8EzUuZ.png\",\"history\":\"/aeariwRHHb23lyOr3AczHz5aIhb.png\",\"nine network\":\"/pXibAQSgIIanNkSg68dUooI7IGU.png\",\"showtime\":\"/Allse9kbjiP6ExaQrnSpIhkurEi.png\",\"tbs\":\"/frMsfc7igYUd90lQXpxuasiN5x4.png\",\"q\":\"/5az50emOs73Ex2HNswUr120J0ak.png\",\"espn2\":\"/iWq7EXFKUO7FYPH6umvE6mB99Ta.png\",\"the cw\":\"/ge9hzeaU7nMtQ4PjkFlc68dGAJ9.png\",\"musiqueplus\":\"/qPld4NPqfUMLYqrNczsTg71PPgL.png\",\"bravo\":\"/4rSnMehNZOLOIikboY9FvPZGAFg.png\",\"abc family\":\"/p57JGkSUBdXbOtqkEKeTnfHn7kd.png\",\"e!\":\"/ptpx2Ag52sYJG6LiX9zBlnKsQOS.png\",\"syfy\":\"/iYfrkobwDhTOFJ4AXYPSLIEeaAT.png\",\"adult swim\":\"/9AKyspxVzywuaMuZ1Bvilu8sXly.png\",\"espnu\":\"/oByKzXnqvTqCD9ctG0sFMm5Mn4E.png\",\"teletoon\":\"/mE2ElX5BSeUhiT3V8qUhLjd3Nbz.png\",\"tlc\":\"/6GRfZSrYh9D6C88n9kWlyrySB2l.png\",\"cmt\":\"/lSwwz91j5O3CLDypFZgiFYZOjp8.png\",\"fox sports detroit\":\"/jySh2X4wkO8jQRSSqfx09joD0Nn.png\",\"fx\":\"/aexGjtcs42DgRtZh7zOxayiry4J.png\",\"cbc news network\":\"/9hqzoCyMCZEXi2Iiz9P4ivmoRQf.png\",\"animal planet\":\"/xQ25rzpv83d74V1zpOzSHbYlwJq.png\",\"city\":\"/eZipPYKRhkp9Hg1Kujyba3oghM7.png\",\"mbs\":\"/7RNXnyiMbjgqtPAjja13wchcrGI.png\",\"mediacorp channel 5\":\"/idQWLSFyRqV3mBsMPDLrvfAP59M.png\",\"telefe\":\"/bBAkTXfNkyjsRVv5ASCgX1WCu0w.png\",\"mbc\":\"/pOSCKaZhndUFYtxHXjQOV6xJi1s.png\",\"tv tokyo\":\"/kGRavMqgyx4p2X4C96bjRCj50oI.png\",\"channel 5\":\"/bMuKs6xuhI0GHSsq4WWd9FsntUN.png\",\"bbc four\":\"/lYk9QLeOO9RFuDz8pnW8NcoEL8B.png\",\"tv 2 zulu\":\"/946paJDhQTQ3iN66qT6niWHBSZ5.png\",\"tv asahi\":\"/usmwgnOfBWuzet8vdWe3dfxXlNc.png\",\"showcase\":\"/aE73SvqF48ZdeMQ0Ls8kzxiikwL.png\",\"sky sports\":\"/xeyAPBYArO8q60Z7UJcXf6WPXBX.png\",\"espnews\":\"/1bRRzGqexnzn5BodK4X4L2SmVia.png\",\"ctv\":\"/volHUxY1MHjSPI4ju7j36EdhR2m.png\",\"citv\":\"/rwuEsuBDb200skfrDTvBiJihMLd.png\",\"fuse\":\"/vkxW1Up7dls1AZIsIjpkNutdxcU.png\",\"three\":\"/45DWFxTFn4BCe1cNmIWK12nqnEx.png\",\"sky living\":\"/k7EOxG3Ul7JWaRq2MzxWM04Gxc6.png\",\"challenge\":\"/qsEPXw9Yp4KIWz5T5HrGBeO5BFl.png\",\"associated television\":\"/hEdI56n4EHSP6kto7BUPEOb6bqo.png\",\"seven network\":\"/83L2wF8tM76nUQHzOBOBAGlPQVP.png\",\"national educational television\":\"/pjQ7a6Pplwm6KmR4kunOwoiqYaO.png\",\"pbs kids\":\"/1W634Hy0VUkaQpEu0BLbLcveZw0.png\",\"tvnz 2\":\"/wGMxGK0IlrDER4GqWKqr3wzOj1v.png\",\"ifc\":\"/iEBH86QxEWoUKOsjDHnpT7qDdy4.png\",\"tv 2\":\"/ynpqIZd5ZhypToHwGhK8WRhJlU8.png\",\"bbc choice\":\"/lBk6aoAEPdbtnhR4H3eouQVq9Dp.png\",\"london weekend television\":\"/iGAUX15FMJDjOwUfRG3Nmm8C1yL.png\",\"sistema brasileiro de televisão (sbt)\":\"/jQ6pbmLCO2mheofZJPPByy7Wozq.png\",\"a&e\":\"/ptSTdU4GPNJ1M8UVEOtA0KgtuNk.png\",\"oxygen\":\"/acDtMnoo5byrtFLEJa6J5BYfgBW.png\",\"venevisión\":\"/vJf1d51q3nOyYDwrtNfnPxBiwmw.png\",\"ktla\":\"/a5w0UazZnO8AdCtyaldeWBbmYW.png\",\"slice\":\"/uSmL90amaWU7yGAaZuhyJipuT6l.png\",\"e4\":\"/A7k2PSYy38NzDPDhajalNWcMQ10.png\",\"much \":\"/yKnRDHAQ0lDt38GS62imutKRzMp.png\",\"s4c\":\"/GOTWuhzHulAcXJMETunP2Wy8No.png\",\"ici radio-canada télé\":\"/32OnRA75a9xn30N2fC44IcCzHkC.png\",\"toon disney\":\"/7ahoCR2iIFYd0p2xbbrhQgnClJC.png\",\"food network\":\"/uV7RnIEWFS2b3A8AD2b0SSklcpR.png\",\"stv\":\"/ntnvyI2vhcEzEv8IHzm2CweEfJ5.png\",\"itv2\":\"/wbGo5nGjF0nFxTajmY5WOZVAEQM.png\",\"tv one\":\"/iEAtsT9WKt6nzBF4sPpwxfNFo80.png\",\"utv\":\"/6PFI9tl5Glq7NLOCqsQXwr4rnlU.png\",\"rté one\":\"/2kjTvhfxtXLnHspCUZQ9cv9gToV.png\",\"sbs\":\"/aQ7chdTN42fz0qtmih3yYnDWp4F.png\",\"tvnorge\":\"/v25T1fvtINzeQMC1AdwFPwOL1no.png\",\"vh1\":\"/5pvUd1Qe8j28nLDrRXCTXC34ppW.png\",\"starplus\":\"/9cEITzi1VyVkre3Fom57vJBOEW3.png\",\"dumont television network\":\"/csmISkmy8gil9Vp0pnQlNZbLdjU.png\",\"sat.1\":\"/tpuoOACmOTDY96gSQ2dT5sd1Q3Q.png\",\"g4\":\"/apAZlt5T2uvebXaEN4f5Zw4C1pK.png\",\"cbeebies\":\"/h05xw1JP88QtiOtvFw8HENlFafx.png\",\"sportsnet new york\":\"/bSXHwrwZ0WknsA56lCynzvZ8cpI.png\",\"channel 2\":\"/goKg57ALHXJnoMBNyKazKOlSNPU.png\",\"space\":\"/5zfwLyBQnPuBsYeo08qf7pR8ooJ.png\",\"animax\":\"/ozIYsP32g0ckOd7e2EqUTNpTwUE.png\",\"wowow prime\":\"/dCN47YS5lugkwts5ellpW51cBzL.png\",\"at-x\":\"/fERjndErEpveJmQZccJbJDi93rj.png\",\"amc\":\"/alqLicR1ZMHMaZGP3xRQxn9sq7p.png\",\"cnbc\":\"/q9ybGdIXHriXSAVynw0DwAahEzb.png\",\"tvb\":\"/u6ij3DirPrXgbvMllZFGdFwyx8I.png\",\"dd national\":\"/mXNmU8Ya7ljkap8P9F5wm717CvS.png\",\"rcti\":\"/qML2Ii1tKVCjLw4n84iWsojuBlq.png\",\"soapnet\":\"/uf48VYnrrEaHBSNLxJbbaaEuugv.png\",\"channel 7\":\"/ibIbm8NsXYrrPLRmSG74hfsVMnR.png\",\"hln\":\"/5Gjx3BpBh0NRagEeuqPd0NFGRIz.png\",\"rcn\":\"/46LNdPSG2gBxy3s3G9csA8vLH8l.png\",\"gtv\":\"/5nhtCgvP4jJ5ZN4ibLzcuw6dUbN.png\",\"cnbc europe\":\"/fVspXHRXTD2mLc1YFzp0zdWCYsl.png\",\"tvbs\":\"/lSdTUbOqNyvGHwiypMJVm0ZqTb1.png\",\"noggin\":\"/jQmuRRuydB4XPm6yTv6Src7LPZa.png\",\"orf\":\"/3Yu4dZFU9eaavEj9U5s5ptl4fHj.png\",\"tvontario\":\"/hDshGefbigrvUnGeRlqSQzUtsJB.png\",\"m-net\":\"/dJ9KWprPx7AXMOxBrGJoadLhhZQ.png\",\"telemundo\":\"/mieMBXx82qgY8nmnyaH0rY2D943.png\",\"cnn-news18\":\"/vyRTawSUJVJpRQbm8LKsYQkNSDp.png\",\"dr2\":\"/hvXnIfzUJwwNryDZxWfKGRR1flc.png\",\"family channel\":\"/uarlhdBymUYsmk0s4EoAaGtxuB6.png\",\"mynetworktv\":\"/90qPHtj2iZXLwq5nMS1xDtVm2YZ.png\",\"people's television network\":\"/yr4HMPCZfulgTrqWqLldnWpoVer.png\",\"cbc\":\"/qe2RYSTCxbPh3jCaM1tk9E4uJZ6.png\",\"wgn america\":\"/kCNFRiqVRMgNWKSWu0LzAIpy9um.png\",\"tg4\":\"/3gyjl5W69islPd2OZipKM3H8QKb.png\",\"movie central\":\"/tSIeioQXU8kPnaMleoaCSrdfEHt.png\",\"axs tv\":\"/m4db3aJTfBhgpgXoiqQRRS2znXe.png\",\"rtl\":\"/ttANGe4D31vZoMmtmolsHSlZUAy.png\",\"travel channel\":\"/8SwN81R7P5vD5mhtOE0taw5mji4.png\",\"hgtv\":\"/tzTtKdQ7vC2FkBvJDUErOhBPdKJ.png\",\"netflix\":\"/wwemzKWzjKYJFfCeiB57q3r4Bcm.png\",\"sky one\":\"/dVBHOr0nYCx9GSNesTVb1TT52Xj.png\",\"treehouse tv\":\"/q0I6cg9HiMt7Jpvpi69cj8t5vOC.png\",\"global tv\":\"/lpB2tPkovzbAbYfyBjJjbptygfV.png\",\"nhk bs1\":\"/hX0BGE9ZWkb3rVAVknHwGbE1MxV.png\",\"intercontinental broadcasting corporation\":\"/jpWs4vSQgf5AeG2zMPDySKgoZx8.png\",\"nicktoons\":\"/xlUUkgMevNvnKXcZ5z7F6R2CaGw.png\",\"playboy tv\":\"/eO5aasMR3jdIE6wtaPdHSyn5R2i.png\",\"science\":\"/orcpefChr3hPSipPcoYa9gSq7ev.png\",\"msg\":\"/w3lRAzzr0AZfHlDICAAPZRn0uVX.png\",\"rté2\":\"/vmTwurCvAuE15Xx5SQs0o0uo0rX.png\",\"irib tv3\":\"/vRLKJDsQPgA8BfBG0pCporH7EZF.png\",\"kabillion\":\"/4R1535w4NgBHAwqUYK7Y2F75rMH.png\",\"here tv\":\"/nTNJrWvGt1DT9iwERf81hSPrrws.png\",\"rfd-tv\":\"/aq8hpQPfhUSEwohm27RAhYLnKZn.png\",\"teennick\":\"/h04Po2f2Uqq19xYHB4mbkjC7njG.png\",\"rai 2\":\"/ar0fBQkxzbBYe4S8zEGnrfZNBnm.png\",\"visiontv\":\"/dtnGkrdsbMsTRDBIxGbciiqWgwO.png\",\"kika\":\"/1dtumlarEVXeYX1BrUFS9qLoG2Z.png\",\"fox reality channel\":\"/xlybmiKc9R93u1xBzgRBVHh1wAD.png\",\"investigation discovery\":\"/3s6YJSYXW9mtJwoszTVI1BHDNc6.png\",\"the movie network\":\"/7UcTAnlDj25ro8LQZJSFfyx4MXT.png\",\"youtube\":\"/9Ga8A5QegQmiSVHp4hyusfMfpVk.png\",\"france 3\":\"/1ePfTUHHvBlVQxa0Fltx5kWhIs5.png\",\"audience\":\"/tmhbFiRpgJFSmza9GTQam8ICyHp.png\",\"tv aichi\":\"/zFZ5KCuvx7K0vP9XgGcidfXjuUd.png\",\"chch-dt\":\"/ea7lVHA90j9UVg5EAEriWkBY6Y5.png\",\"france 5\":\"/8xK6AdNT6PfRyygRvb2MKCoqrv2.png\",\"the filipino channel\":\"/A2ZQ14XP6dm8dfSZiKR6gwUSPFB.png\",\"speed\":\"/kZBWkOGlWsDzcmWBwtRCL3nZERs.png\",\"svt2\":\"/2XpT6uiGyZH6tXbYATb8mTdThSc.png\",\"nick at nite\":\"/oiybiBcUD3kG1XGQd27oWxF3zvF.png\",\"itv4\":\"/9jZyZWsW7ZBY0RfvANDFj18Lq6l.png\",\"soho\":\"/w3WfrSFdWiEr5dJMC3dDMrIyzpp.png\",\"rúv\":\"/r4KMDmIFmjf1xhE3OmbSgaGNxt6.png\",\"sundancetv\":\"/xhTdszjVRy1tABMix2dffBcdDJ1.png\",\"télétoon\":\"/d7roMuKFcpBtQFteOuce4HjQbFV.png\",\"mega tv\":\"/9BWPicebrzShoc8uJFxHLugjc5K.png\",\"rtl 4\":\"/llVa87a3nemOfjSTp8yTR4xQCUa.png\",\"stöð 2\":\"/iSwAMV35QvYvtjEd6Mds945DXQt.png\",\"abc me\":\"/5HcWDo0e9WFKAMx1CQ8qlztKpdV.png\",\"disney junior\":\"/gcKywHY6hQ9ZO8x1R6rSUNQ8P8L.png\",\"w network\":\"/bh9A7b8ejqcE15CKzUZ6Efo45Dl.png\",\"canal+\":\"/n7BRwMvmF2xaSAH8PfJzacrhOU8.png\",\"cnbc asia\":\"/44H155I10tsBBvJ67gO91N5BH3l.png\",\"eleven\":\"/aWWP8dpuGhK3W6NduSkGlo3JbPn.png\",\"rctv\":\"/eFSACJaHlGL4X9UwimqWs2n0URg.png\",\"tf1\":\"/fqsd09CrijoGu6qfoNIdgUQmVGO.png\",\"win television\":\"/su4QkMUe6HngIPIDHZV5WrDSvpk.png\",\"ion television\":\"/eYUrZKBbpdNX4xfqrYYtGXjqo6A.png\",\"mtv3\":\"/u9LyIrxe50xzah32svsarAiYwDd.png\",\"espn classic\":\"/lv2pOqUDdHYjwKD90EV3muMWwST.png\",\"more4\":\"/sO1KfGZzrqIiWohoHgzbvvHEAkr.png\",\"8tv\":\"/maA70URks6mR45DatydS3UuEmlp.png\",\"nba tv\":\"/bqrgXjfeWqTc9oTUgCcT4wTTQwq.png\",\"tva\":\"/gc3SsJUQa7mGnHLds13zzoxG3zd.png\",\"mtv australia\":\"/l98wmIRlhjGZN1nR9YMXi8oQSae.png\",\"the weather channel\":\"/y53akJvADKuINKDfyrbnxk3AUmD.png\",\"ard\":\"/n8OnhOKcr9buScZzEvfJKO6d6gz.png\",\"ntv7\":\"/bYgylwAxgdBaBjVYHgFDrTkXhlW.png\",\"československá televize\":\"/usWXQzUqjcRQ2fZYaKae5G0941M.png\",\"bnn\":\"/2RG1bG0viFfNR5K6IzacRgccKXf.png\",\"nfl network\":\"/bbczSB2PKAbypWMcYxWpNaSjQcD.png\",\"polsat\":\"/wY4JejqerW2SLtbI5poStyHyXNn.png\",\"trouble\":\"/5BYWSGLMj5aG0w7A2UtUjboVRU9.png\",\"bbc world news\":\"/k7mbvioXIHBZqg5nWlLCHzq2OEH.png\",\"starz\":\"/8GJjw3HHsAJYwIWKIPBPfqMxlEa.png\",\"canal sony\":\"/jGGIaOA4YmFeFi1S5krXZ2zaS8J.png\",\"abc comedy\":\"/yoWi43bVJiBKolwn4zvq6omZl3o.png\",\"c-span\":\"/fbDHCA8G5SviMKRvz1W6W0DRgLA.png\",\"rtl televizija\":\"/AlVhoGZIyM56mL07y82SnqAookp.png\",\"kron-tv\":\"/5SgFklnDuIIYNtD8gjCP8IiBcE1.png\",\"oln\":\"/zS4kwaXhwNhMhoT55yOPtQqHxhX.png\",\"fox8\":\"/84wCbPq14btKqZ60shwkf78viMA.png\",\"bbc two\":\"/bfAVKGrJGcKTAndYktB7cf8UlBO.png\",\"free speech tv\":\"/gcLVgJnxsOi41pf1EG3n5cc05zF.png\",\"prosieben\":\"/hComKsCkgZRMWQQnNWu92p3ndSR.png\",\"mtv canada\":\"/21R9OhboglOCwWQOKiS138PZ6fk.png\",\"fox sports\":\"/3sFV9ixObo5To5GoGyEWg0U1qHU.png\",\"kbs2\":\"/nFmWwUAf2D3iAtizUcmkxufaM0q.png\",\"channel 3\":\"/jjndRHOq412g8D6Hjm63rZMxAnO.png\",\"access 31\":\"/vCHdWATtpBx9HYqKvxCvZr6xhlR.png\",\"vrak\":\"/jbVGAEHzFWfEGvP2SEJHw3TWqV3.png\",\"tv4\":\"/qvD4lPRiY8cmgTpINpsWX1aNdcY.png\",\"studio 23\":\"/1PRHnwFCZGuPH4nNSTCZY8A1b6a.png\",\"unimás\":\"/du69v2G2f3MIxuf78hSlfDesIUu.png\",\"cnbc world\":\"/hfmJTKDzTNlV9jYRROE3eaqF3VI.png\",\"svt1\":\"/zFb3TsNzVC8STlkSzbnF1FTHXTA.png\",\"cinemax\":\"/6mSHSquNpfLgDdv6VnOOvC5Uz2h.png\",\"fox sports networks\":\"/cjf6hfzFj7SPhJZAvLHP4Th69OP.png\",\"france 2\":\"/b1cFa5FcHHnpejKiPybXSPcqKjT.png\",\"sky angel\":\"/eLhbh9N9xJpUkVi7qksiT1Rh3kG.png\",\"hot3\":\"/6GFmliU9Dv6pgSPfhMra6a89HA6.png\",\"trutv\":\"/c48pVcWAEYhEFXrWFsYxx343mjx.png\",\"reelz\":\"/ngpAA1R1mOkiwC7RAxTbgbip6DZ.png\",\"eltrece\":\"/j8Sb3Y5vo3wfOjMExmxLF08mLwX.png\",\"trinity broadcasting network\":\"/rYyfBLqn23ZeWkrjlCoxZQONcXf.png\",\"play uk\":\"/59bzfm3ea1STcL3pSmKwxWBsG4u.png\",\"military channel\":\"/7J1OGBuET20pHmy0fZyHXaqBu1z.png\",\"cnn international\":\"/hlKkXccYxPfisU5c4wo6fWPut1G.png\",\"tv3\":\"/kf1R7pshifwcdkhJO5tAEY1wInQ.png\",\"bbc news\":\"/hmOvRkVWqP0vpFXwks8krePxdKu.png\",\"sub\":\"/pjxwmLHvlQ658M2I6NOe9FwaPln.png\",\"cuatro\":\"/nwUbl4jzIFLmbpJgenk5VN4wMp5.png\",\"nrk1\":\"/4TTFfXnGIEosh3EPYX25ERWp1XG.png\",\"mtv base\":\"/lj484gn1gauJ84vZLTlwxVC3gjb.png\",\"gold\":\"/koB7D1eoGWaKvyKQtYsiLeXlSm.png\",\"hallmark channel\":\"/9JTL7HcaiVxq7M6eu5m7giFqaxR.png\",\"dave\":\"/5pS7SazUfbPHBlYy8pnt1y0TgFV.png\",\"dr1\":\"/swq3ovsx3N3hUmqODqIAU8BSdos.png\",\"canal 13\":\"/5JKjWvmR6PQLI4PYh9Lc0RvjiG9.png\",\"tv land\":\"/lOCn3EqluXoP8olsZsJHoWITwQJ.png\",\"tiny pop\":\"/5EduZEQmHFuJ7IN0vD0ASzEdzOH.png\",\"antena 3\":\"/l7MngINTyv0O6mNlwNsUlhQ9iwZ.png\",\"cts\":\"/vYmVFpUIzodAYpVyUzkW62n9Dz.png\",\"italia 1\":\"/2cXinuyZFHdOT0hZW9ZSZpcQOe.png\",\"sky two\":\"/lJMJ2ZIy7paWGAHArgAgVdP3Wlg.png\",\"turner classic movies\":\"/bSp7U5Ok8JVsmnIVYBcKWx2QIsJ.png\",\"wpvi-tv\":\"/vkZSDeY15RRknUSfi4NFRP6RPXj.png\",\"mtv italy\":\"/e9GMyvaguUc36ktS7iSFYP0WLKa.png\",\"rtl ii\":\"/ybTppkzqb3XOWKNcco144BFlosB.png\",\"russia-1\":\"/t96cnWLH1OivW91EXygZDNSmhAK.png\",\"nbcsn\":\"/42DK95aYJKNUHECYorwF9Y4pEWD.png\",\"bbc kids\":\"/jQTIHNpmVPlxJSW2AFGxCalBGQs.png\",\"fox soccer\":\"/bd3HqAbKmzHlEikgaVaS117ONOS.png\",\"royal thai army radio and television channel 5\":\"/yzOyN0ZWBCYYKGJn1T8B7AnP7Qv.png\",\"rtl 5\":\"/xcJeCtgQl8Ren1UiYgYCu8wpxNJ.png\",\"golf channel\":\"/qeAzttzlfeWS0j2bJZ4EmZshRIR.png\",\"galavisión\":\"/hm51VPYcKMALCh6bXbIXN6gfUyB.png\",\"bio.\":\"/6E01nNIHCQK4jyULu9BXrjgrjD9.png\",\"bbc red button\":\"/woHKsYIoXVKQkTnCdHnr6QSu1fD.png\",\"revver\":\"/i7kP2gmsRljZpJbFrGFdiPy1ntD.png\",\"bitetv\":\"/8IrJJJZjn7OoAjBCz5igoPLtzDq.png\",\"familynet\":\"/ajOiVQbESqoIli1a7Ecy7LvpvQd.png\",\"pro tv\":\"/eMJ75CQIE9q7LBE3SWsMtxUlP30.png\",\"velocity\":\"/pwozKkEio5Nh1UvffRGL6rP73Ml.png\",\"kanal 5\":\"/6kmasH8Da0KIhNPFh79FuLhCF5m.png\",\"retro television network\":\"/kxjWiSSpQ6NLQV6lgR4jnxdrPaW.png\",\"kanal d\":\"/qJXllHIGCEUF0MJUUnAMErglUjF.png\",\"irib tv1\":\"/iFfnecuKaF8i5MewrOvTyXfgR1W.png\",\"américa televisión\":\"/u5PIQvo5rpgb97FfhYAYFURKSjK.png\",\"das erste\":\"/nGl2dDGonksWY4fTzPPdkK3oNyq.png\",\"la 1\":\"/dN6GTZyNY32q2onfrvJ2iAeE4su.png\",\"channel 1\":\"/7Xot603KMbXkbW4JOwyH2HCBsai.png\",\"collegehumor\":\"/qtOgRuzVgP8skW96UmIYSIj61f4.png\",\"we tv\":\"/gWllamFWxBczPiFxQzHZd2IAY9W.png\",\"wttw\":\"/np80OW67ZkZDuuQmw9woMYrkHvZ.png\",\"palladia\":\"/hP68W3FIJAyd4bv8UPgmbUPL2R5.png\",\"arena\":\"/ccn7eromR1KWcv5xefUw9rGAVld.png\",\"hulu\":\"/4giYeGORZzztkAAd1HOzrQ4iaLW.png\",\"mtv europe\":\"/eQrvQf4A4P6iaTw2JTpzz1XVHCJ.png\",\"beijing television\":\"/5LU9zEP1SredmscM25lv7Z1xW3y.png\",\"c more\":\"/bWxX52rwNnkPVPtJCPKP5wlD9qP.png\",\"asia television limited\":\"/94NJxCdLsn1tMd4EHiJPv2iUODt.png\",\"pop tv\":\"/uHBySX5Mfre9yK5cjstlxvZIZqt.png\",\"yes network\":\"/3frmQPITaRTkeHW77wZUYUOPSGl.png\",\"btv\":\"/7YOAnseO7SUk9mDlRo08OIDbHTh.png\",\"tvp2\":\"/6SnRcF523HHQ2b0396M6aBMvZ26.png\",\"nhk g\\t\":\"/bH9mwMa64tQeVhmTmkC8Qn9Eoe.png\",\"aboriginal peoples television network\":\"/qqTeEEAdCEfvYlTFXll8uu8Ld8K.png\",\"ttv main channel\":\"/2HVxTrTSKWoFsYB7KxxwMMP1LBt.png\",\"sky news\":\"/9YoK4mgfqsvxqWMNHQcwfWxKxaT.png\",\"fox footy channel\":\"/2dMiTtgbRffsskfaGOejD4TO6Ff.png\",\"tvp1\":\"/5XBLFR7RjHrWrK2MHIpfxpkGei9.png\",\"truevisions\":\"/vckXfsEJ2Co45VjsosM7mk20cqn.png\",\"kcet\":\"/r3yany6hVMVFJ9N6gsKhEWFdi13.png\",\"las estrellas\":\"/6ROTTOqsZo3XefxZAYowukFdJ6w.png\",\"latina televisión\":\"/pJz1mGZl0xKchDHZkuV9XDLW4Go.png\",\"bbc america\":\"/8Js4sUaxjE3RSxJcOCDjfXvhZqz.png\",\"ctv two\":\"/s6BePy1BOPH3199Wkaf6WVnunpi.png\",\"channel one\":\"/f8exyCoSdsESrCrlMgvETOnLgf8.png\",\"caracol tv\":\"/6lbu3Xq8ZsTMrS0AnPfMtPhjxQk.png\",\"star vijay\":\"/E4LG7cTNrM8wmuCVTFkoJIkst9.png\",\"séries+\":\"/cB2uQLRWbK3D7JNYtBCrpKwPGm8.png\",\"tvn\":\"/qMRwARkeUfBNFdLTfDztUpqUEpE.png\",\"max\":\"/as9IofVgrJwSQSPi68nXPuoRRxp.png\",\"canal 5\":\"/kaxn1QFpkkcgacw5DL5lKarz7HA.png\",\"5star\":\"/pkvQvUAnAKn9BLyoWSk7l7cm7cp.png\",\"setanta sports usa\":\"/8koUXsbccaeo14v9bfrPJkjXat3.png\",\"israeli educational television\":\"/zwLZfirHrwWzBsymtN5Gy5TiVdJ.png\",\"france 4\":\"/9v9Mink1IVxPNRZOmCZ8N58BOHe.png\",\"rede manchete\":\"/5PyrCDbFSf3rVa3WyyfstEPQHS3.png\",\"channel 9 mcot hd\":\"/fBUJZboOehTu3izEeECjocUIJyS.png\",\"pop\":\"/m5bolPhhZJ2SZS4XEo8ZCdluCds.png\",\"nova tv\":\"/iBibvLYwkZuYc8lawEgGoY9GyNn.png\",\"hub network\":\"/tL3aAxpgiKdCbSEbmfLKv08jXdl.png\",\"channel eser\":\"/xkcUMRrfd9Mf0Ymcoz7hwOH63vB.png\",\"cctv-8\":\"/vvLZmx02cxpws9IX5P8se3ARBDG.png\",\"super rtl\":\"/nDiZSdUQvqiAUROq6DubHA5pLDd.png\",\"boomerang\":\"/lkMfZclFXosrByxWf459NrXBiRY.png\",\"colors\":\"/1qTv9p35O9i5x0Swg1VbfzLqYBq.png\",\"nrk2\":\"/uyuXWEhdRer47riC1lwpUHYI9sZ.png\",\"zee tv\":\"/a9g7n8Frkiaaf5olShkyhHTk6bC.png\",\"telehit\":\"/aD2mQH0cLAJOHg15J8pRIT3G7Ep.png\",\"destination america\":\"/wEIVk7jOJnEwtKhrS5lNW7WPd4L.png\",\"tv+\":\"/i6D7RMbssq9z0iXdGYeAGtqqWu4.png\",\"esquire network\":\"/czsUqezqDyLlRnI3dUwytoOApp5.png\",\"rai 1\":\"/pLsnP0qF90P4QAXTKGPgDsftKDl.png\",\"recordtv\":\"/aiFTuwrebn6HOoNq32Ytkm3MEKZ.png\",\"canale 5\":\"/5nhlFNs8ASHZij5ZNvF8sXwpLAL.png\",\"star one\":\"/yqrTS5shXIjd8iAbUKUTqImgPQi.png\",\"sctv\":\"/3Zs6z9zA8mELGA04fnFfy2zM3lm.png\",\"tv9\":\"/d32YycZbbedFEIH1n0c9mkxcwew.png\",\"dubai tv\":\"/pBS3SKvsoXDL1SEGjkseZ0vtwqn.png\",\"rai 3\":\"/eRLfW6GOHrV9rOE0YnUIYzIUjyz.png\",\"rustavi 2\":\"/h0HAku8p8oV4X3eN7SRbideIrYv.png\",\"markíza\":\"/e3v7L2zlVEO1GLH7pv0f1CNFIeK.png\",\"tv puls\":\"/8gwxEEoFc0NdhJ2PFurREGQTb0x.png\",\"sky arts\":\"/jNJA48OhbJjDJtlBhmegJsMcAkH.png\",\"rtp1\":\"/iJBaPuHCVjROCUXZzGuon3oDqMC.png\",\"tvp\":\"/AqrV3Nzf7Ofja9asZpkUxkBeOlC.png\",\"vtm\":\"/383jAo1fLu8FTnbjFRY7gS4s6K2.png\",\"rts 1\":\"/7MHxcb0oaOEEerv1TAKMwJNS8Pn.png\",\"link tv\":\"/8FrUVceUlrDQlMpoO5igxUPHuc3.png\",\"onstyle\":\"/iOxSRZyRJ4W1eFxE4KjEAIzvgEQ.png\",\"sahara one\":\"/qsm88BnftOCFmNa2NJFpmHpV5Pj.png\",\"azn television\":\"/y0nd8l1EWIqttcOlgrobC4GrzMD.png\",\"mtv pakistan\":\"/e9GMyvaguUc36ktS7iSFYP0WLKa.png\",\"srf 1\":\"/txYJaM2VKunNRk3JooH1mnsVSOg.png\",\"ant1\":\"/xHFlnu2Mn5OhekVLExa1XYtcqJe.png\",\"mētele\":\"/kW9bMQqnMFtPHvdoyCDpRqh7Gxt.png\",\"ndr fernsehen\":\"/ycqsY7zsvi2EkLAdAKn4rozvOpf.png\",\"asn\":\"/cTgn4vc0AOHeEzL9ozg8vNmmrP7.png\",\"wnbc\":\"/7OE3m5MRyJqBygASzLJ93TvvSUK.png\",\"v\":\"/l3TVdYRGHqcDWMXxHdofswgynW9.png\",\"the nashville network\":\"/9otXwFJUXTRyEY9jMSoGVhZXF5d.png\",\"itv3\":\"/n4a1RiUD4F9lr31WLAEg8oHnci2.png\",\"tele 5\":\"/2T0uzcONwa6w34MWdlN2QzJ08sr.png\",\"tolo \":\"/qsjFn5sFnqbIEgzqJapYRjvZ0i8.png\",\"pax tv\":\"/tNnUEFZV7RmtUsHxVG3RmQn2WUn.png\",\"abc news\":\"/sUZ0N10Qbg8zbZ1IIgyUTfhY6Ds.png\",\"cp24\":\"/1o1hmmTRejn8gUH6IsvLFwm2f6S.png\",\"astro ria\":\"/eD4l5QOBKFGojiWgXlQ3qQE7ntO.png\",\"rush hd\":\"/oxLhbVU6SrEBB6CGK1lw6oyVLEp.png\",\"televen\":\"/f0Ylm2274535HSBgicsuVpdnx9Y.png\",\"ntr\":\"/a3DUk1Tpaubu4vGUITUa4NQSlGZ.png\",\"kids station\":\"/7V7vR2yOXblLOZH0qa1Zcqs5BRs.png\",\"tokyo mx\":\"/3qFArHw6nrFIdkH2tPht701sNhs.png\",\"yle\":\"/9KQP9AEEqOpSVYgih9tqJKCkA1w.png\",\"nyc tv\":\"/9eeH7SsakrYk1QC4KsUEug324ZK.png\",\"dtour\":\"/fRv3FaOynrlFKmgNKxloMCH6BjD.png\",\"new england sports network\":\"/tilq1nUQUDb7LNh5hcHyI5Laj0N.png\",\"nhk educational tv\":\"/1N9ItImbsdy4AZcfs0kUlyYxQKV.png\",\"diy network\":\"/bg1njNAqzcSefrDF9e64hQvRMbG.png\",\"discovery real time\":\"/qaOBgzNyO8imgMFCuvMW1jtmM3d.png\",\"ocn\":\"/x1PpeMBZ1bJzitJyPuuUMRN8u2z.png\",\"nikkei cnbc\":\"/uZ5iuDKHe5N0LMXsGfe3UCvkeqQ.png\",\"yle tv1\":\"/5WItBySbMUAklcJMVv6k4GEOR6a.png\",\"čt1\":\"/4fuYKKu4XvCIgFh7Sg2j8Uzgjmv.png\",\"nbn television\":\"/hl7xdx55m7dySt6sElrKZBOBp3p.png\",\"fearnet on demand\":\"/uEbZJTy9migMWBlIeAQ6sHq30om.png\",\"avro\":\"/uxRel2YeOgNiRwo28NIakZfSKi2.png\",\"future television\":\"/6JuHPsBeKOV5uU0LAUQJEBjCjvZ.png\",\"super deluxe\":\"/1r6pLSTR1h7DQTXa87UXoiIi3Er.png\",\"tvr 2\":\"/AaJV5H4lZ9pMXBHyyimUU4OrgfS.png\",\"the sports network\":\"/eEtN7vNV9ISMft8TYUVvWfq95OK.png\",\"sic radical\":\"/zo5Fh88xy3wjk6HqiDVilStaTnh.png\",\"yes\":\"/9yWETECjfnsxZ9UtviBZXaZ087k.png\",\"mega\":\"/pd1QoAjnhfXZutHOQ7KZtu1ZWkO.png\",\"latv\":\"/n5a1CuFl9aD5cPNC68NUrBEG4Dv.png\",\"vpro\":\"/96hCWEsfmTZQsoUap0CcG21qaA.png\",\"good food\":\"/zPoHdGnUH4MpIeS6SFqr0tr0wzw.png\",\"the movie channel\":\"/psTUtm3E7tIZ3D8IDOtfaRxYvix.png\",\"arutz hayeladim\":\"/oAvXUCqp03ZYqfwokrCbce2EG2F.png\",\"my damn channel\":\"/cpVjXtmCE1zUrbim16qOsypLsBL.png\",\"trans tv\":\"/7k6LEMLTtmiMfSqZ8Ai7DMpl2W3.png\",\"smithsonian channel\":\"/meU2LRnHcSr0m0Upx5loxHWETqF.png\",\"daystar\":\"/5y8rXfTAWTU3WYy6DSbLWZGiYcw.png\",\"indosiar\":\"/rINnk0IoS0jd4gMHCOsmRTD9iJB.png\",\"arte\":\"/6UIpEURdjnmcJPwgTDRzVRuwADr.png\",\"chiba tv\":\"/8XNtModCxAwtvLy86b3phkE1jge.png\",\"vh1 classic\":\"/nXv4IJqVHwFcAnDyJMv4P8UbrwS.png\",\"nasa tv\":\"/uy6hjTBQa1qK5iP9aiJJGXKb4wj.png\",\"bloomberg television\":\"/njT7K8UuwxAmS3ZgS48n79OJcWX.png\",\"telesur\":\"/2ygPNCW0f24ACFG52nfvtOCKcZK.png\",\"discovery life\":\"/q0s7tkK10OiIYM5eHO1dLRpOem9.png\",\"aurora community channel\":\"/rTdsdWYeHNrXvYlOejMYgJlm8cK.png\",\"ctv news channel\":\"/2A5L0nQnOHk0lF0FViwxX8W4Fcd.png\",\"tr3s\":\"/iIW5CDRQMlYkFYF6YP3AV3TTwI5.png\",\"eurosport\":\"/AfhbW2Y6X9uwoZAgoP0cOfSPoH7.png\",\"sony entertainment television\":\"/jfGyjgH1i22xCQGFoQTPZyFLVR4.png\",\"ztv\":\"/rvg8MLiM3Lzzwi7CTd02Wh0kUP4.png\",\"christian broadcasting network\":\"/oscTUqzDq0xgrACJOuOlUy2WSyD.png\",\"ren tv\":\"/mh5PeHiXKewYD8UYSKao5c7GFW9.png\",\"canal j\":\"/uMMKGhrUmfXtE5SXLCyNtH6gJsV.png\",\"lifestyle\":\"/9MBltiYrqFNgMSecwmFPrf1UAfS.png\",\"bold \":\"/ryeIUr3wnwUGnKZ8egBDN7bKgn4.png\",\"home\":\"/rh6xH8UIqx4o3Jb0lMZArcsTurq.png\",\"tv cultura\":\"/zfSn03BXad9VadOBPe5HcGrMoZW.png\",\"al jazeera\":\"/vtEHxJKWedfOLGoibApOrFBBTES.png\",\"tmf\":\"/zmukzR1VC0ee3sAYwE0kKUUDaoP.png\",\"kqed\":\"/wARyrEcUQko2AzJ2JepJOr2s4fd.png\",\"kqeh\":\"/gnRkWdoYbBSTssZzgVYsMPDcs59.png\",\"one\":\"/dpgEQOqag77UYovQLM1GXNWywnH.png\",\"cctv-2\":\"/kTWfqWGvWDzMTkmVGVEc363DjrC.png\",\"venezolana de televisión\":\"/bwfWL9rt7QPvOXlflLFKG82RmoF.png\",\"yesterday\":\"/gWBrjMLsPypHyQqin4cXz7i7v7.png\",\"tv nova\":\"/twuMX6vryssdv8oY01ZD5eNTKLt.png\",\"m6\":\"/ebCZs9U5Kq1GqOFBT1tlH1lZd8p.png\",\"panamericana televisión\":\"/a4MhsRdI14SBcQYXioukdJTNw7R.png\",\"teletama\":\"/yhFNPfWqQTOj8olwR4Gk2H5397X.png\",\"tvp polonia\":\"/rCzuIh0Z0PLtBnoiQdP4VgFMPYP.png\",\"jnn\":\"/3nGVNDvVqavpmlVk6jXDPtP4JSy.png\",\"tv 2 zebra\":\"/29jlOx5toz0FQWAC4xvLHThRasi.png\",\"funimation channel\":\"/a5oMoSOlNaH6AydiqLm6Gf5dnAx.png\",\"tros\":\"/cqDmmhm6hajmnPYBIBNzdVrDqK7.png\",\"kcts-tv\":\"/9qE1ID398SaX9zYiRiPHdZfPTbt.png\",\"teleg\":\"/6chdmJlRJQ2LgsGbUuUsIQG9fXy.png\",\"kgw\":\"/oEn2me5z7dMiuiyb5Fhq0NthIc2.png\",\"mlb network\":\"/rN8HUqQIGsHQwTMamuQmeAZdqY7.png\",\"great american country\":\"/oPiqGNF9lEOuyZVRfkph9YY2dEB.png\",\"show tv\":\"/8kD8Spe7RlZOqNV54bIvLuAtprO.png\",\"geo tv\":\"/5lRMiLqqjuuBFOfYi9Z0m0pCOKS.png\",\"outdoor channel\":\"/iRg8Zh8V4EB3KvWFDrAfuwmHfS0.png\",\"star world\":\"/cGR5NFbkIsJayT8lcvJyiQmZq3a.png\",\"veronica\":\"/qmnO61PjyHfi2HqsAQszL20GLpx.png\",\"starz encore\":\"/kZIZEsSe4GmqtaX01QZSxMpR8mS.png\",\"vara\":\"/el8phAL6Xa6vXvvaQNiELmEOgx8.png\",\"kro\":\"/b0egFPlnPdqnhhWywwKwpdl4GXJ.png\",\"alpha tv\":\"/jaY3I8qSvcymAOMsafafhFatC80.png\",\"rtp2\":\"/r5zfKDhZ33qIVV1QiVYRmrbhd3x.png\",\"nova\":\"/krIUkWv3LOiaYpU2vUWbPrqTS1k.png\",\"muchmore\":\"/ukImteHLH8BfaRwKH0zQG0yUPV6.png\",\"w9\":\"/e5TTp11V850EmQjoeoTd93NyDHS.png\",\"star tv\":\"/dEXIDBT111hJgZjKKHKB1o7jhQQ.png\",\"rete 4\":\"/fWh7OAc6hGan6h7gYiPu6ARdAdN.png\",\"national indigenous television\":\"/oTYxKyXcetIc1LXmvPHI60kXrYz.png\",\"vox\":\"/tfrd4tN4KOr9cWmMBI1xdhWc6v6.png\",\"super channel\":\"/rqGf031msXZRkHeoRu0nRlI8vW8.png\",\"nbc weather plus\":\"/kOouDwONltKwZ7LACyEe3E6MP7d.png\",\"star chinese channel\":\"/vdDLFYlbeQmrQpvxbIroRyYuIrJ.png\",\"tvk\":\"/onniCKpgZl8VEVSS8oa08Oas9jT.png\",\"nuvotv\":\"/zHWUAxaRHLGdjPp9OOIAU3wjDBh.png\",\"mavtv\":\"/l8PRX4B4OVVZvpIv1WspN3ZS43i.png\",\"national geographic channel\":\"/q9rPBG1rHbUjII1Qn98VG2v7cFa.png\",\"kfve\":\"/izJGbSnzWPJgA7OgoaOUzW1fbON.png\",\"american forces network\":\"/rqa28aS9PAUfBsRYqYRQGIqYTqP.png\",\"cooking channel\":\"/w7zRUMYgEwdOWD9tAjHqFtREbMU.png\",\"4music\":\"/zVmoePlyh7CyDS5lRPBvtwSR0mp.png\",\"viva germany\":\"/3DTdPqvewDwM6SXOW7Ca45EX2HX.png\",\"chilevisión\":\"/xU0AW8Lf6mwRGiOAwzUPM5XNbHT.png\",\"sts\":\"/7746GOkRkVW8UviV1OqDNAzdpgE.png\",\"mnet\":\"/8R0iHi9qYaEvWWv9XH5XoGpuRJk.png\",\"azteca américa\":\"/f7ff3vL4zYHHqZD9OhhI7SoWB6M.png\",\"cctv news\":\"/K9ZzEc7RdUaVzUnaAiYT2QIvkL.png\",\"ary digital\":\"/eX0FNufESHTD8lFBwP31DcieOCe.png\",\"tvm\":\"/bpMDF5niZ0OBMsclu9vJ7JOjnIh.png\",\"sun tv\":\"/5g8LER5X7tvziu3We71m9HnBmy6.png\",\"sic\":\"/lgc1ScGTjapJJZOZezszceuxRyR.png\",\"fashiontv\":\"/hjznlXXosdr5bOJ9G0yU0rTgHRX.png\",\"tvq\":\"/koirj3XCAq3J6OK3YpNepJIq7lO.png\",\"tv osaka\":\"/6foQmoPac7WCDnDuDbuCZklmDuA.png\",\"antena 1\":\"/rXFw15vvldWBxZuJOy0kmrkGFfO.png\",\"own\":\"/m8H0hZOpkskkIWeqRdiuP2CZOgq.png\",\"planet green\":\"/aYCHvH2wTWT930WChun3xJhj79e.png\",\"kbs1\":\"/AjEOZyoAyvZ5iAqA91P0ra9X420.png\",\"polsat news\":\"/eokeifaZXb84ScRE59fciPmjEsI.png\",\"magyar televízió\":\"/f5Sn7vW86OR8wsoLmc0qPi34hXB.png\",\"rtl klub\":\"/fGmCnh6tmQlCllhaFaHbKf6dSL.png\",\"zdtv\":\"/46lTha6b37IjPBYl0Fp2lSSuDP2.png\",\"disney channel latin america\":\"/rxhJszKVAvAZYTmF0vfRHfyo4Fb.png\",\"pts\":\"/qEQ4L4VyAUM4Ea1x1CMrS5JaPOt.png\",\"comcast sportsnet\":\"/l7UYVQv4kzjEqFwVjpJspvZeoE7.png\",\"rt\":\"/1Jaw3XrH1YIljfYKzreC5vccham.png\",\"tv 2 charlie\":\"/gstdG1UAEaAQSTYi8Wtx5fDXNoz.png\",\"canal d\":\"/s3hn7KdtX26EYXFa0MRtLtoG8P6.png\",\"5usa\":\"/aiNDCNGIJagS5D1lzgyheQYdkya.png\",\"nrk3\":\"/sW3AjUOXSsmsmzZJl9M9vHEGqKd.png\",\"tv3+\":\"/k4o90Kn4dlM463h1f7p4pGgXv5O.png\",\"muz-tv\":\"/2OyKNnKFFViTCfpW6eMEHyoYSj7.png\",\"h2\":\"/bd6vDgU9S8RHZQhjACj007vSnR6.png\",\"the pet network\":\"/yCcrTmJqJv5tN7sBwsLUuBGvKmz.png\",\"antv\":\"/6XpO7mkGVQns85enn0EmWepTy9V.png\",\"viasat film\":\"/gNycHYrLZc1QEcUadiVsTVfi74x.png\",\"sjuan\":\"/A5MluV2bOnC6X5pZbEQ5KJR6doV.png\",\"sbs 2\":\"/hDsDVGDpMAF4RYDuLReoCiuy98k.png\",\"life ok\":\"/9OdoJOEMff5ZSM1iisjmPJWIXTs.png\",\"2×2\":\"/k8iLqMFhoJLznlycK54q63faZY2.png\",\"ctv main channel\":\"/zqFRxhKTBqiQ1GLghwJUG5ftBJr.png\",\"bs11\":\"/JQ5bx6n7Qmdmyqz6sqjo5Fz2iR.png\",\"action\":\"/tJxGplLFqCjGARhg8dFWRUXTmFi.png\",\"trans7\":\"/w31NBcd9jhVFCp6BFzhhTpFyGSL.png\",\"mtv brasil\":\"/tPc6YzgcCoIwfWoILOmj0kMwBHe.png\",\"west tv\":\"/Yp60FMzSW5tXpLEDQoBs7AF7Aw.png\",\"trt 1\":\"/evw4LkwnmgnUHDQUUTPwpuhFsRp.png\",\"formosa tv\":\"/qSdro8ifVBU5wcLg3T2tdT6u694.png\",\"br fernsehen\":\"/oD5KfK3xZxJfbNFMhbpuovdwEi7.png\",\"direct 8\":\"/4Dn5JdscAUXQQqVCZ6lDk8IVUVg.png\",\"fox sports 2\":\"/inrSGtPRteNjHWKgmv0cJrWaPIt.png\",\"sky cinema\":\"/eia3wASVi2KULA1hgy0gXIeSihs.png\",\"tōkai television broadcasting\":\"/pX3gDCsG8WcQvQbCNJ37t5w0HyG.png\",\"tvg network\":\"/sdi800YFyFgxuW4cuhefD2X59yp.png\",\"bs-tbs\":\"/zcWERoR0KacvffK7PBeAdQfcfmI.png\",\"jtbc\":\"/44I4aVlasm8Blb8WPGXTkMYuZJF.png\",\"channel 7 \":\"/lj8g9lWrTmRvO7yGPeDoNS8taDd.png\",\"band\":\"/1bpeEUnCwQUrU5XQLP0msaIwv4e.png\",\"bandai channel\":\"/xGS6thPrtg4hA4bvjyKuWcpPSrk.png\",\"tulip television\":\"/qfUzYnpnN5bnd2bqGUH0OlAMell.png\",\"cnbc tv18\":\"/sAz1kkzKmYNjTAYXVnYCzkHBITQ.png\",\"armenia tv\":\"/4Qe34F4EZ2H6s9MTKKNjvFNtq6z.png\",\"tv chosun\":\"/7KzMozIYj58zKLMAaybYxWUWjdg.png\",\"tnt serie\":\"/oPPsO7TGHwxVW7h8gsi6BP6sHYs.png\",\"nhl network\":\"/7T2utseIB8pm6YNJTDRBmsNTLto.png\",\"byu television\":\"/6SS8KHz1KH594IG5hom9bL3DQI5.png\",\"big ten network\":\"/4KrfOZXvC3AGcKuAI3Xm07xvnwo.png\",\"nebraska educational telecommunications\":\"/4u8WeGjfq1Hox7SiGAm5e9urYX8.png\",\"epix\":\"/9aH86hGHVQfvhAqrDRv1EINoxua.png\",\"all-nippon news network\":\"/mlcZ8Y4Q71rKTMhH7ItHH2ZrCVo.png\",\"mtv germany\":\"/e9GMyvaguUc36ktS7iSFYP0WLKa.png\",\"central television ussr\":\"/eMCbJfoNCBZlbDKQO6NrfFyfYng.png\",\"crackle\":\"/bR8S6Fjv3VGtEKyKF5lvvRJ5xfw.png\",\"mbn\":\"/7XKVnL4rDYZNBcTfalthDYsnYzs.png\",\"mtv asia\":\"/e9GMyvaguUc36ktS7iSFYP0WLKa.png\",\"sbs plus\":\"/90qhJ7ek7FmvhxGciWqZIi5R3Ed.png\",\"ocn movies\":\"/2f7wljCY0l5716iwnrGmhZm1oJj.png\",\"tv joj\":\"/uTNAiA16CZpw9IqlvizCswtXV2m.png\",\"trt çocuk\":\"/plxq6VJWQkcJLiB3hXhGbyBosvQ.png\",\"tvh\":\"/vEUZNYjuSn0iTQyZ3SLNfVgI9Z8.png\",\"theblaze\":\"/6hv8xCPpFAWuRBwcc17yPgfBEc6.png\",\"xtv\":\"/joesfMvq4jbfQQzYSKBl9y0qhXi.png\",\"la7\":\"/682kUhTAnoqLnjVrVCCDNWhd78f.png\",\"kino polska\":\"/bKJKrhuxL7C7PcLzRZ2LIt8lxi6.png\",\"canal+ poland\":\"/snQBlVJrlLu8mgvvNf7cn6NUP9g.png\",\"hunan television\":\"/6WVfgvW4r3jiw6v80jflHSQr41j.png\",\"domo+\":\"/hf9ytYXqJD3nltQmLIjfxm4HAQd.png\",\"mtv poland\":\"/e9GMyvaguUc36ktS7iSFYP0WLKa.png\",\"samanyolu tv\":\"/ocg7ggjBkHjBoWsivNoLfcepQHi.png\",\"mbc 1\":\"/oG6DMW1SajkvYpbMAZ9m2LZURYx.png\",\"mnctv\":\"/iocCLzVdgFJnrY6jgBcG7t0T1XO.png\",\"kompas tv\":\"/c3Sb8OyrV14FtTJFi0uwrgQZYf5.png\",\"rotana khalijia\":\"/iynB9WxVpc1Tbean0wR1ufueI7r.png\",\"acasă\":\"/p7iv8u65nbOCdS3RlMUnJKDVhUY.png\",\"prima\":\"/rkXrvlCHvu9kholozdXInpFIcD4.png\",\"powned\":\"/nmLUhNof9VsJrBfxWLjGVQpZWiQ.png\",\"tv6\":\"/2NWsdNseiHRbgsxNeSs7pawiJym.png\",\"tv2\":\"/ze9D9aLekVWZqLdrD1YeBKBgwz8.png\",\"mtv denmark\":\"/e9GMyvaguUc36ktS7iSFYP0WLKa.png\",\"turkmax\":\"/904DQtkXsaJgda0Bd0lUJ6cqWgn.png\",\"asian food channel\":\"/bInMzS40CwuFe5VshmnjJllxPnQ.png\",\"astro aruna\":\"/uMUHnF7I3MdlqAY0aGNCanlzdZ.png\",\"music 24\":\"/u0KvrWrvKdXOn5WW0Mx9CNINkS1.png\",\"antenna tv\":\"/jjvypoVqza69K2KOoiXAAwWvIqB.png\",\"abu dhabi tv\":\"/xkgPOy6LEhz6sxS9JUZi2SuZJA0.png\",\"zhejiang television\":\"/jXR61DUeQu4jdryz85cjnCFXNNv.png\",\"etv\":\"/zsAoHKZoisS3UkGVE1ypc00610B.png\",\"bnt 1\":\"/3yGshVnJkeBZebtnAO2D6NMadK3.png\",\"liv\":\"/gkbzGqaC61olfJUxGQpQNVzPNO4.png\",\"yle tv2\":\"/pbnLDOMP0nFRgyXMehuAzxh4Y5w.png\",\"quest\":\"/e8yVrQ3fBKOZdLGHVyJz25X0NwC.png\",\"channel [v]\":\"/343n8epNQbuZzOvafIvan1WJNzA.png\",\"bbc uktv\":\"/rVzjodqFFtiltpYXyXqNfdXJ8te.png\",\"fuel tv\":\"/owTQrkg5emUr1bUwpPOOfdyW1vv.png\",\"espn australia\":\"/h0cjpCMfeX9irSu3g6aLLniKP7E.png\",\"nelonen\":\"/uFxen4Fik1EbOLfzTQC80zZvoeh.png\",\"mtv russia\":\"/e9GMyvaguUc36ktS7iSFYP0WLKa.png\",\"plus\":\"/vkrkqstsMEqFRoux9ivPHKeFQ4m.png\",\"eden\":\"/4KwxxBQ1d0sJCXRbG7tLX0ORk8T.png\",\"el 33\":\"/e3wM62UyjolroXeYPxfiay1WVFe.png\",\"ltv1\":\"/2QYKk9W10eTizbGlkhFwVmpJM4N.png\",\"ichannel\":\"/b4SB7k3AzNR0rbwZYDHgJEixXkY.png\",\"lasexta\":\"/AtJXlAoj0ITHKDN5EPJZPHJgLxI.png\",\"manoto\":\"/nsSvyXHfEIrZEnWo0lzFICq9QET.png\",\"thai public broadcasting service\":\"/s5BTEEGw6NyRd0RiJlzbK8ofdnT.png\",\"sportsnet\":\"/e5EpXQONx9dHwohXfEurZJEZgqR.png\",\"whyy-tv\":\"/6atmMY5TxsEpGJJjAeJNnfn1Kwn.png\",\"prime7\":\"/5tA0UNpqiAb8RhZ0lGJMXoMgK4a.png\",\"amazon\":\"/ifhbNuuVnlwYy5oXA5VIb2YR8AZ.png\",\"wwe network\":\"/iTD3AB16Bp9NK7HEVr2wOw43MPJ.png\",\"één\":\"/tbpZK0xCfDzNPdzz5s4L1OJGjCS.png\",\"playstation network\":\"/oLrbwkpqM9ShxO67b0HQTgT3tkW.png\",\"w\":\"/Pp7UfEzEkXo1blroQ7PpcVQBqw.png\",\"hr-fernsehen\":\"/rOebGShjIWMSBYOihHDSVdV3q5a.png\",\"sbs6\":\"/77VYaPlhcGqnRbaMwi0oAcKv3dk.png\",\"fxx\":\"/hDLXRZMBOCbpVYpkBbIlLvMXgdX.png\",\"sky perfectv!\":\"/o4LTo79SR55GEYQKFZjq2MUDb7n.png\",\"vgtv\":\"/dfOrOrLjVOcebSX4203B5BlrwKD.png\",\"vier\":\"/1JGVNbqHVbTOUw8M5WPf2Goiq1T.png\",\"nat geo wild\":\"/esXeePrsUGpF6mGQXs3jImz9QoN.png\",\"al jazeera america\":\"/yTwWYAAwL2Ag4wxHVzOnhwkjwUQ.png\",\"star channel\":\"/4FdEFkYde4AyjOoRTGw0gL9DREW.png\",\"cw seed\":\"/wwo3PZyBpHL3Wz8eg4cr3kqVZQY.png\",\"aol\":\"/i6qYMS1mYYQQh0INmAQPz20wzdu.png\",\"bbc first\":\"/gLNh4QYTeYCKJzhZwzzwgMEqI3n.png\",\"mbc masr\":\"/gdNW3Sdhx4tMhOhp4rr3Ok1iJqq.png\",\"jiangsu television\":\"/lVrjBzC9COqah57JSkjx6pjOXtd.png\",\"dragon tv\":\"/neufSFU0ZrXxA3c0JRNpuNXzLdx.png\",\"duna tv\":\"/bPTVfSbxQsQJucD6ev7eF1c5n6j.png\",\"yahoo! screen\":\"/117TR6CuHY1Tapv3T81Z8je6fvN.png\",\"hbo nordic\":\"/sFWkLyP2ps3yUOajuqaUEWxpSWP.png\",\"sky atlantic\":\"/6es7UmBjk2HTSZKq3NbtAxYEGCx.png\",\"rmc découverte\":\"/xU5g31CXgsP7BhcDtXx9yW0Emrj.png\",\"vimeo\":\"/lYL7PahejU2LKpMVixqksxnQ448.png\",\"fox premium action\":\"/jjXxcZ0wMkQcvwocDkl4Y9jOQ7f.png\",\"ocs city\":\"/6toCBtsVvbItflTy1gg8qhLwx2v.png\",\"lifetime movies\":\"/dNUQ8xO2D13KfAke4lmT0a6JYHH.png\",\"canvas\":\"/i5MdIv07gHYRPhYlQ6mm3RrATXd.png\",\"sky uno\":\"/NN6klPGA1vVxbldSGZIllYIeK9.png\",\"fyi\":\"/jQ5GW25gdpP0ooM22Kt6Oo8TRl.png\",\"olive\":\"/nsERt4AAKUZaB9FafjZhAATa3jV.png\",\"niconico\":\"/rU3iJ1dHKneqKLqZ1H1lc7eNy4T.png\",\"net5\":\"/9nDYpxzCXDmcMWQbfeF5R7APPNv.png\",\"hbo latin america\":\"/xiKKr4gL7TM7Q5Rn79LrYFeRjV3.png\",\"multishow\":\"/5Ane2hnYZCjfVHV0OsHyzdtTtSg.png\",\"contv\":\"/4RcQmKjjI1dwfQLpbaszYuTdDTC.png\",\"gaia\":\"/kym0UnNZNRxGHdZsFBUQNCCRrcb.png\",\"google play\":\"/wc4mcODIyf07UEnqEMusJS009v0.png\",\"kabel eins\":\"/kTU5bdSiPMxHkOvQsTWQSZeOy2e.png\",\"crunchyroll\":\"/81QfupgVijSH5v1H3VUbdlPm2r8.png\",\"dmax\":\"/vltk9XNsceQOxO8onLEyJyUXXfo.png\",\"sky italia\":\"/dNuhKIiAChEJdGA7TXQgwqbFU6y.png\",\"foxlife\":\"/fLvIov11xLjV93nK92D9NQ33l7B.png\",\"mdr fernsehen\":\"/iIppR5wX7s2soFTHZsHV5mgQ9Or.png\",\"el rey network\":\"/9z6vxDNyom1T9WSDd9rCwaVLQLs.png\",\"hbo europe\":\"/tyoN6zoxMJ71GBddxVkk4dpaeze.png\",\"antenne 2\":\"/1JnQqStddzcIoa6XYB7LqJbLx01.png\",\"télé-québec\":\"/c1YCG42hBUQruiOKRT7xD6erGZd.png\",\"3sat\":\"/bwqI45tuLt3qygttnZUz4Imh1dC.png\",\"tmc\":\"/mG8AujLo1qacorLWNXqHqMCxlPM.png\",\"wdr fernsehen\":\"/xtK4PjzICkOEB2JSD9hgeQLow9K.png\",\"bs fuji\":\"/oMtJVGvqLAyQZvYTvjDIcbVEzwD.png\",\"kbs kyoto\":\"/j12pSWPsDBxE1mmIpB7M8VRzk78.png\",\"gnt\":\"/ykNd5NwABd2hmcwvOrTrneIPz2G.png\",\"tv8\":\"/nX4NrQzkUMjciGpripgTNLMPnJB.png\",\"tvfplay \":\"/iFXRW7tcLTfy2UYufxg8B509OEb.png\",\"naver tv\":\"/zt8MWI6nc20BCTViJPrtFPJFAY3.png\",\"ketnet\":\"/3IdQwXxK3WlYfc0wUumwBIcN43K.png\",\"bbc iplayer\":\"/zg70HfOG0FHyKEvTGp7RIP4z94A.png\",\"itvbe\":\"/cKcGa9lsFn0mSY6TDcskNJ0wDAh.png\",\"swr fernsehen\":\"/f3WRCoSDZyXYmG6mcg3adgSAnfe.png\",\"kansai telecasting corporation\\t\":\"/1J33ZvSff1VOEt3aC8CyKmj9GEy.png\",\"ictv\":\"/sSlI7H7o85Lixv3Lq5hYUeBGjHr.png\",\"stream.cz\":\"/w1dYHHZ7E01XThKEFxzGC7SrnEm.png\",\"tlc uk\":\"/nDCnUdHNseEmnVaek4u3bPl0bZR.png\",\"mtv lebanon\":\"/26t1txCTRfGjN2qW5WZegWuTiF1.png\",\"toons.tv\":\"/est3feuwDI2L6VxvJoDIa33PzJi.png\",\"gbs\":\"/wi1TlXf4tnnnEA5m3DkbH1X7iC9.png\",\"mie tv\":\"/iJZZIOdaKz7aYK01BTUo001ooPc.png\",\"up tv\":\"/6lijJgGj3wKZMKPuMUyh8wpUMtx.png\",\"nt1\":\"/7Cpds3V9PLm104O3iyjg0jrTatz.png\",\"travel channel united kingdom\":\"/pGzkI87fhsp8w7Xi4uPhwMVvyJh.png\",\"duna world\":\"/yEp1BDU3rbvF4IvZ6mzHaJ6LhN4.png\",\"npo 3\":\"/wVbxJBRSvSdGeLTm4z7WOBgTD1B.png\",\"ortf télévision\":\"/oRNBLezTiOW3WMly2rOqXoG5a5H.png\",\"russia-2\":\"/liqQQATn4eTB72ZK4dFYfTv0F4x.png\",\"ntv \":\"/tbUMYj4Mm8RKiaVQ6vo8zjUKGsO.png\",\"das vierte\":\"/1quKB0ElnUT4SZbadFaMBPMysjc.png\",\"hum tv\":\"/xcQRcWC6mfhpSMvb47Vkr1uqvpc.png\",\"stb\":\"/8VidRVpoPUAtOCWfqgtq5bMnyD0.png\",\"sat.1 comedy\":\"/r2SY6PDU4wZQYdqffPrygomz1O1.png\",\"yle fem\":\"/A8L0PEH6QEMB1SszGWuBaRdOghT.png\",\"servus tv\":\"/9KwJgZRZazjR7iTDPQAKpyfuJP3.png\",\"rtl telekids\":\"/qhLwkbUoGnSYSH1OBIFpREgDckA.png\",\"rbb fernsehen\":\"/cqkLRyzEMuvaUYWWZGVCdZWwdVz.png\",\"gemini tv\":\"/J8ukrtA7oCTidkE8tqJfl3vDeb.png\",\"avrotros\":\"/wXrkCgSBWhF4cjbztPoWtDtOJqj.png\",\"eo\":\"/yhWwg3mVe9TAb6oXeiGHc2hPz2q.png\",\"twitch\":\"/9zEimOdB52l4V4zUa8cQ1pp9fxm.png\",\"divinity\":\"/a2L3Q0HaMNWv8XJa91RcLK48T8E.png\",\"mbc every1\":\"/cFhubSOjMZXKxVbg7fylXUk8HPb.png\",\"facebook\":\"/6LEYZhup5GJmUyTgXTk8q44F0nJ.png\",\"historia\":\"/eRZTyNxfFM8PCdquTDvyHERx0wo.png\",\"z\":\"/cy7isDv4qfKnOOWEOn4BHRlqVEr.png\",\"movie extra\":\"/5CcSuKdeR3Z1MCQmEVlCZq5mE34.png\",\"1+1\":\"/xctncKucNjPJW800COSuGF8J1MC.png\",\"stan\":\"/1akCJMjyZsiS4v3NTJD5Y0LFZ4R.png\",\"redetv!\":\"/3eAsZvWd1bZF35edX61xDhFPsTh.png\",\"tv-3\":\"/1U8FtJNwKMiUITng9A6pOY6jnt7.png\",\"sohu\":\"/2Ew6jjFlEPxeubJEijHZY14Z27p.png\",\"cartoonito\":\"/q7mFfgYZU6FK45npeHgXux4bHKK.png\",\"canal brasil\":\"/xrNYfnO8sop22tP2R75g4JzRC3c.png\",\"freeform\":\"/rsz16keQ0hiBWYpaKIFspsMwuqj.png\",\"zdfneo\":\"/5xti6WP3i8AoI9o4Wl813JA98CF.png\",\"rtvs\":\"/poAWYpzmESfFdh3uwSCc9TkfK7j.png\",\"kyushu asahi broadcasting\":\"/zph6Ed1k57GnwNkwjKSLo6wh3uk.png\",\"addiktv\":\"/2TQGuYdP3IJuKLps2k7NYxbNySK.png\",\"sixx\":\"/xSfWRN9kd5mUp65JEKRD5IVmgcB.png\",\"discovery family\":\"/zza75igMo6rkTC3IV8gT6TwXMNl.png\",\"hdnet\":\"/4uR2HZZyKf0jikSAAGnY4TYYDIq.png\",\"jim\":\"/hfFPMSsWBn9gFBQKSNnIohMo8Es.png\",\"club illico\":\"/sqJrxmxTASr5HaDGfARp2Mv1Cm0.png\",\"canal once\":\"/qgsS2eHcJNWLCEKVQ9x9WWYcqXl.png\",\"moi & cie\":\"/b6KxdWSWYOXiKsEYTHMKhaqNiip.png\",\"go90\":\"/a0PSZBhWKk0A1ID14u3DI4QrE8A.png\",\"atreseries\":\"/vk0DiMkdXej24FSQ3oahd6bHhoS.png\",\"discovery asia\":\"/vWHt63dgArLtqnsSTu744G6g3sS.png\",\"hbo asia\":\"/e0TPhobVyxQ09bu9r5qj3rQLxkj.png\",\"pure flix\":\"/9YhctbTDK06WO9Oye580bvWsnrF.png\",\"telemadrid\":\"/3BrhXENv3fNvyGHsB9xNCat2BuS.png\",\"prva\":\"/kcf1jXy3LI49k1tBB0qskDczkip.png\",\"ovation\":\"/taRXRfIg76AzEGYMorSzB30Cpse.png\",\"ici tou.tv\":\"/gnz7VENDBQfKeaJWDdT4f8O0bpG.png\",\"televisión de galicia\":\"/xFCWp2bNmwQFG6QAF4Pij7TqAiZ.png\",\"television maldives\":\"/1rLr1aixQ3Eu55neO1hYprwXbpK.png\",\"timvision\":\"/6TbgPzmPzCnJ69fxSicj76d9io7.png\",\"repubblica tv\":\"/cOwQwxIKPJTuvwsxDOboCC6gRF8.png\",\"domashniy\":\"/pgxodwZf8IyWo65M89wcbZouM1N.png\",\"tet \":\"/uJtL9RshoQjvDWy7vqJI4UuUwvA.png\",\"abc iview\":\"/yQrlu1TLTWLc4QSCsC1N7M7lGoz.png\",\"iqiyi\":\"/t2HmORL2WgZdUUWyK2QOVIU6lwv.png\",\"videoland\":\"/bkC8VkgQoolKh9rs1YeaiIIf1Rh.png\",\"viceland\":\"/qatKbG5JUVlG3TXyTSQmakI3Pdt.png\",\"chiller\":\"/n3uRSQoUznYxEHoedb3TMqJyKwU.png\",\"crave\":\"/xlVkcuR2NqruUHh8gPaAw9BvOCT.png\",\"abc spark\":\"/ofTYpgSeD9g1ngurAigv2GwesDT.png\",\"la 2\":\"/r9zAeL4pm0miwpOZGoKYz2Q837j.png\",\"zdfinfo\":\"/dQPFVeKoJHy27pF18gqpT9xzIm.png\",\"bounce tv\":\"/n6jLkog079ad4w0Vm9f5sBfmC1X.png\",\"history channel italia\":\"/aeariwRHHb23lyOr3AczHz5aIhb.png\",\"rooster teeth\":\"/dMmhDfPq3Ulz6AIJ9G2ymeTKwUh.png\",\"la 5\":\"/fBvBN22sqohV1yqsbOY0YqUvt4d.png\",\"stream tv\":\"/tteR5Nu42tXwynoir9Tqnl5nMlM.png\",\"mediaset premium\":\"/mKI1LBsgZPPKSmy4kq4hXTZI7Dg.png\",\"cctv-1\":\"/x0f4gH2NOQpE77XF4E8IrrwfoKU.png\",\"cielo\":\"/dMBZYliXeBx3LWWLaa3no5qbUD7.png\",\"rtf télévision\":\"/h6JrmEHzmsNEaOo16N3YHP2GHHl.png\",\"reshet 13\":\"/tyzqjTqeUob43G4h5RorVftFQOt.png\",\"tvnz 1\":\"/tRUJaANZCvxWQbQfdWcP8UvOjTG.png\",\"itunes store\":\"/h7YSa1EqKYtz5ouFRguyb1dzCHA.png\",\"zoom\":\"/e88FdLtfoPvIWgECmjO3EbqgKgL.png\",\"dr3\":\"/8tkTPwcCC6wRctDgT53Q67KIFIT.png\",\"la une\":\"/kR47hzfGe3zFsFipHTuC123qyCe.png\",\"telemundo puerto rico\":\"/mieMBXx82qgY8nmnyaH0rY2D943.png\",\"viki\":\"/xKVCbqy4jjb8JgUZzQfS7JjRAUQ.png\",\"kanal 2\":\"/vfrFks7wIMo61gkDTgHuXhfebcY.png\",\"cadenatres\":\"/eylWpcQpVqVeNfMQz2afE4sSmgI.png\",\"ava\":\"/kHJSF8VxsrQmhh85LGXoYXBVC1K.png\",\"m1\":\"/kn04GbAmCv95bMiUaIQG7q4BH1n.png\",\"crime+investigation\":\"/vitUTPUae38gzMRl8aMVecN0Cme.png\",\"nitro\":\"/yce3GSxIR0USFkq2qA2ZTUqaJWr.png\",\"asahi broadcasting corporation\":\"/ca3Qw7exc3RPSd0CQFOZuSNbGjF.png\",\"fox crime\":\"/9Jvtpn5dHx3GwkLZFF3Uu7Qxt2X.png\",\"tv wau\":\"/4F4WYqokszsuLaWXPKdlzAES8xX.png\",\"youku\":\"/jGumA5iP8eXdQ22EOFY6kWrBzza.png\",\"rai gulp\":\"/nzYlSSVEia1cj51VU28MNZt70IO.png\",\"ahc\":\"/607cHSuLR2HBOOTbE76Db2KDmOS.png\",\"sky 1\":\"/dVBHOr0nYCx9GSNesTVb1TT52Xj.png\",\"npo 1\":\"/eDObRs53SyWCHlnFoKTTnwZGPpB.png\",\"youtube premium\":\"/3p05CgodUb9gPayuliuhawNj1Wo.png\",\"ami-télé\":\"/n9Ew7VScCbnPccoArCHgTFnohOo.png\",\"pogo\":\"/qAhtyf2OX0bowig8g8CgG9iViRb.png\",\"flooxer\":\"/vTpEiwbk7a9qi63xCwlTxhJ5k4W.png\",\"cartoon network latin america\":\"/nDFWFbAHEZ8Towq7fsVCgv4U245.png\",\"tvp kultura\":\"/8N2gw2i6CvoJgHQisNnfYESGKrU.png\",\"btv cinema\":\"/j2Sk7rRQ8Q1J48OnoRrXGP3XbOj.png\",\"pptv\":\"/oISsIyupFNddsKTsvL9sdBpJnyz.png\",\"ncrv\":\"/fNRriPhwrnqLLTtus56ZVMrTUta.png\",\"family chrgd\":\"/bvD5SchGvYFLyZlsvaPryPDTjl4.png\",\"minimax\":\"/9COoUNYnEb6o8oGkVOrxEq90Ovq.png\",\"axn asia\":\"/3Xls5ATyPM60HWKiDuVSErnQdzk.png\",\"sabc 2\":\"/t1OEW9w4G6X4yXVn4Wd6dBXeAWm.png\",\"abematv\":\"/tEYzkOmnBQ7jmxqdzJ6nAk429aO.png\",\"infinity\":\"/8a4NMo0A1PYOeLv4v7dOazarcZj.png\",\"seeso\":\"/7LtW83ODEmIxDDwGBTVo22A0vcV.png\",\"epic\":\"/aA1J3TjNvUCs1zEssJpwbDeNF9T.png\",\"televisión pública argentina\":\"/3jB8XdFxuxgmY6O9db5I1vyvoOC.png\",\"tnt latin america\":\"/nLB04d4R5UrlH3AsYDRAcgAbtq7.png\",\"tv5 québec canada\":\"/5KU8oRvXX9I1bWdiATsSlYqx6em.png\",\"discovery world\":\"/qNNNVIAnREE8m12VmuXd20OGqby.png\",\"wapa-tv\":\"/olygrPxYGt9otF7BuYOCAWuUsBA.png\",\"el nueve\":\"/x63mh7ahQ4mReHkjg08BPzLjiWO.png\",\"7two\":\"/7JvANenUas1w596l5ikfSBksDN6.png\",\"sbc\":\"/artkCykJgOAh6lAjfrNNgANasgk.png\",\"&tv\":\"/qmI9f9HOyY3L9ZGMgFfUExBJ9yf.png\",\"prime\":\"/uHro7KWaW6h2Dj4ILFKYC1UJbd2.png\",\"tv brasil\":\"/q6CBUNLpUaqvkI4ukrcmHdlIHn7.png\",\"spektrum\":\"/4j1soX8JkM0W2nrlkrO15ir5Gy7.png\",\"viaplay\":\"/6SLkjdD4tzmqI5TIxZ1BLlNqMdw.png\",\"al saudiya\":\"/zIGFsV7DK4A5fnxBEmkrIlS2MGK.png\",\"hbo canada\":\"/lFYhRrGIJ1x2wuTrEizMtsdRxFP.png\",\"now tv\":\"/kwHeVfONZfDBfBwToWGYcYkW1U3.png\",\"cctv-9\":\"/dU67Pko8o057r6mea8kAHEO2Gyp.png\",\"cnc\":\"/nmrpwndiC6uTS4uksPwk4O75y4U.png\",\"tnu\":\"/egMex7hlhlrCFbt0xB3H8PaBisv.png\",\"c8\":\"/sxXmhPMQeGeYwbh7RKMqUztnHGY.png\",\"trt haber\":\"/hdvgTXHZjvza8h0QHI9K58CARyE.png\",\"bilibili\":\"/uA84wxiVlJJRbBkymIdKTw42zk8.png\",\"carousel\":\"/mC98ktCFvsDb5vrAOOhLozTVn71.png\",\"mult\":\"/dY0PUuBx2KXhXl95DzDY5ZLMylr.png\",\"tlum hd\":\"/2rx79qHpPWEEeBIvIOrqFDPJas7.png\",\"canal sur\":\"/yI3v9EhMP2Mt3tdIXuz6E8t1YN2.png\",\"tvr 1 \":\"/axE7fqLKR0oN9pC9HwrtcrTbjcm.png\",\"ukraine \":\"/jLFuGLkwOQHAekvU7CmXaBBj4JA.png\",\"arte \":\"/6UIpEURdjnmcJPwgTDRzVRuwADr.png\",\"mango tv\":\"/c6GPQWwbXDuD59pGGutCBQ1T711.png\",\"rt uk\":\"/eMZoX8lgiMv5kbxnRbnaeJ1rPg9.png\",\"inter\":\"/fTwKj4hZMMGCFLHSglJOAKnvNH.png\",\"star world india\":\"/cGR5NFbkIsJayT8lcvJyiQmZq3a.png\",\"fox premium series\":\"/uPVVosI5lB0IyBzXpAPzkzoYE6w.png\",\"fx brasil\":\"/sFK0I7f90UY5o6BDC1CLhJXKs6R.png\",\"zvezda \":\"/3nVHeDt2xktjuqyIC7VXWOEOOgH.png\",\"kanal 4\":\"/pHeWzT9WtPst5TWVLFjeWQo53ZT.png\",\"fox latin america\":\"/mGIwo5uKaPMK4sRNwSwRl9nmRtd.png\",\"fdf\":\"/p9guqIXM0MWzk2F7uGPvJU12ase.png\",\"canal+ family\":\"/slQe62dRo5kCuiwz7oLVQAHN1hi.png\",\"dplay\":\"/cllBOlqfzv9t6KpOmMcpanhxBLI.png\",\"funk\":\"/9MozrFdzzDNo9AcpkN4cz3TkGrF.png\",\"nolife\":\"/55cfTZPHKBf1pReaVG07kLsfnXn.png\",\"be mad\":\"/4H5aVdCbQcKd8qEkJIX9QA8xPWW.png\",\"line tv\":\"/eBK2WsrRlpmLpyoeR09I6fDRJie.png\",\"hgtv canada\":\"/iQgW9a6kOgMbxCVYB97643ICVDn.png\",\"osn\":\"/d6D9pUDthmVYnmS7dmXj669l6Na.png\",\"studio+\":\"/oRlKS6G0JJ7LLQUDfVgM7x1YCxk.png\",\"fullscreen\":\"/9u8319x0HeSCxWyzko8KjNFgNhZ.png\",\"facebook live\":\"/ausOAbylt6Id43JF1q3rfeYw2ao.png\",\"nick pakistan\":\"/ikZXxg6GnwpzqiZbRPhJGaZapqB.png\",\"sab tv\":\"/3hNAuxQ7LWXfbkTlMptD7shp7UE.png\",\"cbs all access\":\"/7d02Rw9EDMWba5yeM6EZqPydmCn.png\",\"al-nahar one\":\"/j2DofV0EVjP2TWC8aGqPAAcBsnD.png\",\"super écran\":\"/vTzKinhC0oGQ5tNBGDf7EfW0PiE.png\",\"rtl2\":\"/oG6mx3igu1POada9WinACUpN02o.png\",\"tvi\":\"/sn36BiLSsKi3QcWvGQUn8KVfl0P.png\",\"7mate\":\"/8AjZwKDE88I37Wf6OoOPJVsTPoP.png\",\"lifestyle food\":\"/rww2vgf35bZ9a90GVCljJo8JUoh.png\",\"astro warna\":\"/qBMcf4uoXu6LH4TEd9SFDB8T56M.png\",\"astro prima\":\"/h3fFqUqdoT9YCsQxbuU5x4ehmmB.png\",\"astro maya hd\":\"/hOC8CA7WyCDg0xvI1JqhAxbi9pb.png\",\"blutv\":\"/aY0a4ZsUyG0zYKDE9H9I1eT4qdq.png\",\"globonews\":\"/xlJCivVutt9N3GiXGVIMFTGUxGz.png\",\"rts un\":\"/sISI8xF35P6wIjYmx85RHCRljy7.png\",\"rtéjr\":\"/tk5BBwAt7oJrlZCNhXYAXooWt8.png\",\"maxdome\":\"/W6znAbTNCBQUwyCwwxuqZUeAcz.png\",\"al jazeera english\":\"/9nAXtejiDQYqkeIWWk5eulSo2c.png\",\"bs asahi\":\"/79NRmO2Kj1lJ5nqdf8A4q1T6Edq.png\",\"hot8\":\"/j4twJk4F0EmQVzNa3KbgX4NPB5o.png\",\"manhattan neighborhood network\":\"/hOg1wHvttsEOT8v7DxmA6nkzfw.png\",\"fusion\":\"/q0OTdtI98gljdgwHHIdNW2aaJZz.png\",\"crime & investigation network\":\"/vitUTPUae38gzMRl8aMVecN0Cme.png\",\"hulu japan\":\"/5EbNwVlVEHGOkSJFw9cjhaHJOhY.png\",\"ici artv\":\"/u6n4LTVDa19B73PZcEQAiLqL6CG.png\",\"vrv\":\"/u7mpqGD2mgtgV5QdV1ZFXh0xdtK.png\",\"one 31\":\"/cxBpog9yHJTIclmGsojZC7tqwlH.png\",\"évasion\":\"/rJ6xicSW61rop8ChUgI7r8vp52M.png\",\"astro shuang xing\":\"/spr25hXf651DaXjTHoynIO4plMz.png\",\"mediacorp channel 8\":\"/mgbUgCcgbpxzXD928ELSymL5JzO.png\",\"sony pal\":\"/fOGv0UuTWkLxp4aOTw3s9C7T2Rs.png\",\"suomitv\":\"/q78UuMAjpSIZnYMWo72KYJHbFm9.png\",\"orf iii\":\"/alCTY3pXKHFPoPLgMaJrFirkhsu.png\",\"ebs 1\":\"/vM4FN2WU2JU1mCiZw2BtqJ27zxR.png\",\"dptv\":\"/8J6u2IEM9gPQrYhLfq9GGfXpswY.png\",\"obbod\":\"/gEnWY6u3CxtY0HxecNK77Pi6vBz.png\",\"hot vod young\":\"/g9I9S69LEwTOFJeSRF6NlF01Lt7.png\",\"video pass\":\"/zSqGTh3kRtv3yeHRPJOk4BvBQew.png\",\"rtv 1\":\"/z2rWC9f1T8J9O1MEHNXWmncCYFL.png\",\"npr\":\"/jc6Wxu2us3rgsmbCJRLvfS1n53Y.png\",\"hd1\":\"/sZvjj48224okQHs7HduRsNpikAx.png\",\"nove\":\"/97Ztf6tKA78wgg5yZPnr1zrE7Bn.png\",\"elisa viihde\":\"/gPkqpaV3B9JXb0jtzTqEjtA6fom.png\",\"kyknet\":\"/2tNeujuBe8MR40hoUE4FLYA7Knl.png\",\"13th street\":\"/o9bWCO9zWVv9hBXTFmyOVFMyLU5.png\",\"dsf\":\"/yFiGMVOuLjaTeBOWbdARBpkkHoF.png\",\"comédie+\":\"/qtIj2PTkOImRGPu7k2NdDKtTiKp.png\",\"einsfestival\":\"/s125v6BpZSyKDUDPZUOpfnbKwph.png\",\"rtltvi\":\"/oyOihoeQw7wcMx59hNhVwOI0wyR.png\",\"family jr.\":\"/zJn9ypM6cfI5APVVnpl0a8Bp4BU.png\",\"rajawali televisi\":\"/vxzYcVxYGgHdWMVrZTewxxcrntO.png\",\"kidz\":\"/lHspOb8qSkK7P8UN02oOgXtSG6I.png\",\"dove channel\":\"/vfTImdo7SUW6yJRM8XiKiNeajyx.png\",\"ap1\":\"/dzfymvDFyoCfLE6Ot0I2YVAToa5.png\",\"yle teema\":\"/lt1agd2pOlDmhaSmwSAklOxNZq4.png\",\"mbc drama\":\"/v8IDgKmbU8J3VQxl8mrtIphwesh.png\",\"really\":\"/eNQpKWZ9fmN6YIuQTObFfbFIWVy.png\",\"tagesschau24\":\"/7XPIFSf35QHP4TMEtNXtjVLTpJf.png\",\"ewtn televisión\":\"/7rbRK6vILJFsQIIQGRNEFvNsv3y.png\",\"fbs\":\"/suqJy99xiGr8PV7eQovUFRgquYT.png\",\"ocs max\":\"/hqYpZX7DKht3HTXbdi5SSCNEPxF.png\",\"odisea\":\"/lp1kyWPAjJTgUdqVKMaMWuzTE0I.png\",\"blim\":\"/zavliTuAnVSaip9OxbsXTs1v8uN.png\",\"real madrid tv\":\"/puHOYN9L20sxZXaifbQQJ0mgIOS.png\",\"true4u\":\"/cGSND9e3N4A2LE1DMdMTUwCLoLA.png\",\"kbs joy\":\"/djRcqN9RaCcXCi0JGvcjISASVvU.png\",\"jtbc2\":\"/bYcQHaavcJQV5XXc5LmiH5e4iSy.png\",\"puhutv\":\"/q10rgiHNUffVdAs3neLZZmXUsca.png\",\"on e\":\"/bMjDKMox1AgY8byELQIgrMdw4r9.png\",\"alhayah \":\"/eacM4a8Soc2FdgkBkmzH3j4au3u.png\",\"apple music\":\"/nrHnK81jlSAA530PfZlU7t3p9wv.png\",\"corrieredellosport.it\":\"/mZd26t06bKCSn7LZ05ezTanhqxc.png\",\"tuttosport.com\":\"/gIhV1CB1aGQb32LNESiSQ27ehqS.png\",\"nrk super\":\"/7wYa0aLdSPtTloukX4uyXKpsIVE.png\",\"channel a\":\"/yZiuYgxiBGJAucKmcBIy7TnVkSv.png\",\"al-nahar drama\":\"/sJwKdmChunUfVweZHYRqmAcCRrL.png\",\"a plus\":\"/akshdZKWozy4kAgHLfdJMhg9gz4.png\",\"gmm 25\":\"/ggKRyZIgrGhVM0fszfYWwzJIuio.png\",\"kktv\":\"/a8E9RFxhWBDGfwPBKiyjDe8ZrXt.png\",\"vrt nu\":\"/cQlIILsumz45oaxV4LsKAiZXTvf.png\",\"rtl plus\":\"/zchm9Ut5NFhxkrID36QTXaMaUNZ.png\",\"all 4\":\"/1yd6L2IUvuNmzvYYtd19rm9l7H0.png\",\"ici explora\":\"/lRhRKVFzRQGeESBAETOZLcghtuM.png\",\"erf 1\":\"/7fXyaPO11CDsGas0nhvyE1lBprN.png\",\"rtl gold\":\"/zuBvtzbEUWXRx19rZjQSonYO0pt.png\",\"kabel 1 doku\":\"/o7zjpTAiTwvYSXeVpCjLPLlQ0IH.png\",\"axn\":\"/AjR6Ug74xJYMhtpxGFusVGAHfWd.png\",\"ufc fight pass\":\"/mDWJAHRHefTi5BQnq9EbvGsYQkM.png\",\"tencent video\":\"/6Lfll43wYG2eyereOBjpYFRSGs4.png\",\"stargate command\":\"/fPXvpCLOBpAkhjjsyXQLFazuAqL.png\",\"ozonetv\":\"/dvRKuaOBydS9P73UqdEBhfX9L9L.png\",\"canal viva\":\"/kWvptNESFDqic8JITEsmfhiajuE.png\",\"planète+\":\"/7wJmmffmEUVrvypzWI3AfL5xuro.png\",\"star maa\":\"/3O4SFCgyGPJYmfWs51qPmk3T7lY.png\",\"red bull tv\":\"/ibCAL6FyzVahFxVO6XB3TAYbp0R.png\",\"arirangtv\":\"/z075mUlUUMe5WcEDA5wU09mtRsi.png\",\"vtmkzoom\":\"/fHqpMM2uYdN5wg8ZCR43XXK1CwC.png\",\"13e rue\":\"/e28DXTUjaK6SKCpJpmICQXllswx.png\",\"myspace\":\"/fweyuLaPpCgJQIHd0XcMEsoQ2u9.png\",\"canal vie\":\"/zJ9wiyWwHjf15OWLEccBLK7Glsa.png\",\"tv setouchi\":\"/qi4L1ffRabF5LJqVimCaaUYpeSj.png\",\"la red\":\"/pbwM9lVxbxJbFLroAxfc1zE3LsD.png\",\"tv 2 fri\":\"/otdpdzEUOp6WPo46TSfuJuRgD9t.png\",\"paramount network\":\"/4knr4ozp2IQrA3SMQlLSYKcM3ML.png\",\"viafree\":\"/e6QNxf1eUhD1V2IRXOAMbG2dDfH.png\",\"discovery channel\":\"/8qkdZlbrTSVfkJ73DjOBrwYtMSC.png\",\"unis.tv\":\"/iYWhs21Qn0gIH3jTlA1cDTSqNYH.png\",\"plug rtl\":\"/r8Meoc299hN06gDaplv1aJWzJkn.png\",\"blackpills\":\"/aXVvr4Aw9JBj2SWrEjb1tJaHYPC.png\",\"tv vest\":\"/8ZzYfUSyC6O5S4p1qG1Ha5fG9wS.png\",\"techtv\":\"/pNE1La0x9MZJAFe8Z2QTEMG1wmq.png\",\"g4techtv\":\"/fmHYPuOlLRah8sp7ZYUNg2LqsMF.png\",\"the new tnn\":\"/g3qXB4IDKL8sIk9yDVeKlsfLeFT.png\",\"casa\":\"/a0ByNgmzVO3NpC0P7gY5xntZo3J.png\",\"dekkoo\":\"/qD8575BVoagIUosW3JQo25HYShP.png\",\"altbalaji\":\"/zZ8gquIrrBvDGyMMcsSgArRuzyh.png\",\"mtv latin america\":\"/e9GMyvaguUc36ktS7iSFYP0WLKa.png\",\"orf 2\":\"/yMnlNdwOHo3YrqPFXVYgvz3pimN.png\",\"the great courses\":\"/p8bhSRCzDu60XrsQXjWfoArbgqn.png\",\"canal algérie\":\"/ed0NjMKYAG3SPGdNJjaJEBmFqs7.png\",\"etb2\":\"/u2g0SE1ZpEzrJv9wueKAHEbfrTt.png\",\"universal kids\":\"/p7CrTUDgneTfikaSP6hAlFcbmqF.png\",\"dtv\":\"/tmLTjM3nAzu5I1YhO4aiJmOh3wT.png\",\"la sept\":\"/7LE9NhjHzyQ4tOjgdKAn5eBZerk.png\",\"hoichoi\":\"/qVqRSAy79VGjfN4qyeypuS5vLX1.png\",\"#0\":\"/7odcr2uwJNhanhEaFOnITogxmRX.png\",\"rtp play\":\"/mB5QFbQhbJh4haHFP50TaDjhbQ1.png\",\"fox españa\":\"/mGIwo5uKaPMK4sRNwSwRl9nmRtd.png\",\"viutv\":\"/mmQOH5hoPd9ZieQPr5FFsjAMgD8.png\",\"ard-alpha\":\"/tdy3ElLfBqKuN5gRgvhBguEcTdv.png\",\"dr ultra\":\"/udkodZN7FY6iXsYQjPDJwj3ex7H.png\",\"bnnvara\":\"/tiBiiquRvGhxXteossLAWQkaFA1.png\",\"tlc go\":\"/wjQaV8ijywhBHMjjNTTiwUBtw52.png\",\"wpix\":\"/uTdbKD11HIWuUv7chni1TGiebEk.png\",\"télétoon+\":\"/nLFQkHAhQXlhLGRXJTbuULYJWuv.png\",\"yorin\":\"/vTPFIDfCy9dO0j0nORq8g2Qv08W.png\",\"channel nine\":\"/azP1gQxvW4ssDevqHgCvN6mCeaH.png\",\"irib tv2\":\"/uMqUppxmwSGDnOS8PROETr8wNcE.png\",\"4fun.tv\":\"/kB3LXsRMnzy2CDg3TzXA753Uhxj.png\",\"ortf télévision 2\":\"/AgVBX92ALf2fUZweiyhSKJYhkHo.png\",\"bangladesh television\":\"/lI6c4QsJrfqFA1wTIrQtigM7jF.png\",\"pick\":\"/zvM889g8dYVL8mFmyZVPLrtf543.png\",\"t+e\":\"/wa0IRJ2W3X12ej0wKLHRWM1ApyU.png\",\"iflix\":\"/ysdz7EX24pmSFlLzF224rvBVhrz.png\",\"astro oasis\":\"/nkObiKyTKiQV1SbIw7AGTAL3vUR.png\",\"6ter\":\"/oKO8f2L6QRNu7HpeyDr0SfgabfK.png\",\"mtv japan\":\"/e9GMyvaguUc36ktS7iSFYP0WLKa.png\",\"kanal 1\":\"/ovdg7mo5neFJK4bTkh4Q0JFfd5X.png\",\"universal channel\":\"/l6ctvLJ7vFeLF0FIBMXeKpQEEPt.png\",\"dc universe\":\"/eOL4PkiC0zkDpxKFQhBnmCtwx5p.png\",\"dom kino\":\"/pnUbJqJnxj2HZyGhU0RAHYxH974.png\",\"tv centre\":\"/gOua3AtNAQg4PWbTRKTpwWLfeIN.png\",\"talpa\":\"/kejyjdzZg04SMRkRT6w8piOmXOd.png\",\"xtvn\":\"/hcXvuYIsKJHhqd7Wzh4rmOBNyDk.png\",\"fearless\":\"/zKdWJymRhBLgd7T1lrKMPcc0hdn.png\",\"showmax\":\"/72uE6rBXSuth2k36cGsvIYxm5sp.png\",\"o channel\":\"/u0605fwa355v0MaeyP0HgvXIdNI.png\",\"toute l'histoire\":\"/x805Xveq5h4eSUZ9rbgXJe8R3ie.png\",\"vidi space\":\"/dM96XxgerPiGJHlhnXi9pprnSU9.png\",\"tv2000\":\"/tbEkWpDqFqHGlu7CYyfbjomskMn.png\",\"ici rdi\":\"/TpM7iJC7rcUgIBYb9FwLslr6p0.png\",\"mtv nederland\":\"/e9GMyvaguUc36ktS7iSFYP0WLKa.png\",\"rsi la1\":\"/biCq8cxwDErIA6of5Mi1DHRv9U7.png\",\"hallmark movies & mysteries\":\"/5pVL5llSGxKMxEDdfxmCDJUs3Dw.png\",\"dk4\":\"/eSMIKk4bpKR4g2BobUmnzHT80GS.png\",\"nhk bs premium\":\"/9iLW2n7zFTFwiCmlXPzZkwNZnsc.png\",\"canal famille\":\"/2UbgDDv9cHAdGrznUWEi62M3RTi.png\",\"tqs\":\"/iq7ZGb7RN4JCaWvuCvn3VngFCy7.png\",\"razer\":\"/eEJhNL65DYewmplr4LWHhaiCqYw.png\",\"nickelodeon india\":\"/ikZXxg6GnwpzqiZbRPhJGaZapqB.png\",\"fox footy\":\"/ydzHHnyZsC60RViiNAvMe3v5la4.png\",\"fox sports 1\":\"/xAih9wNDjqvDmX9O9BA1bekRHQr.png\",\"country music channel\":\"/reGaYh8WHArHeTndzA4ikkkNByt.png\",\"fox brasil\":\"/mGIwo5uKaPMK4sRNwSwRl9nmRtd.png\",\"fox business network\":\"/7llt1tSy6CNazFNUuWSNvssfWI4.png\",\"rtm2\":\"/jlBKRggeBhdxvVW39g0iCqRxdOH.png\",\"nou\":\"/vqA3Tlc2rjS4ZnpllDYt9XpN5Mj.png\",\"nhk\":\"/y96FXAPOfjLnQYTd9HLEP5toxpO.png\",\"curiositystream\":\"/pO2jjhCt7UeCSqDJJ4O1AtunmxE.png\",\"pbs digital studios\":\"/ah3lNfMwcr1YOV9NZuCFEhwdrLf.png\",\"astro vaanavil\":\"/zSoyLUFwe5n13qG3aIphWEnKXaG.png\",\"htv9\":\"/9nWl4V9190sE76KwIR5BpHIikHb.png\",\"sundance now\":\"/tKitog3eIbbdLMnWNZg75ErhJr9.png\",\"facebook watch\":\"/rtn9QlZo54aW04pBVbtDqNfO0Iq.png\",\"atv home\":\"/toGJnf4MniOfRwl8ki6nGm5U9ov.png\",\"sbs fune\":\"/ue22MrGpagp8u0dQ1bRemFSCI0e.png\",\"rts deux\":\"/8G3g6cWhxdqCQeyvJHpUVZrZtqh.png\",\"čt2\":\"/mSGMeLPTTCKKB9DVMJoFFzWNUqd.png\",\"rtbf.be\":\"/rWYlPrkpIK1ZvDhpYTc03umbZYb.png\",\"la deux\":\"/tSheAzX1u6cVVvzxaObx8gyblHV.png\",\"jednotka\":\"/6bqAV4y12zSroTITdmPO0vYCpda.png\",\"crime + investigation\":\"/vitUTPUae38gzMRl8aMVecN0Cme.png\",\"tv rain \":\"/dEv6BcTZWHCBXCJZiVTBQQGUM2E.png\",\"brazzers tv\":\"/axTJr1j7O9xV8RhqeCM8ESPNJTI.png\",\"noovo.ca\":\"/t1ETxTmqkcGHtz3cRemxR3zdxgK.png\",\"npo 2\":\"/4GaXwPcTDWaZ83a9hOt2fTgh1kQ.png\",\"tf6\":\"/hKHAEkO7gW3UA9VSVI7z13eN6Yn.png\",\"lotus play\":\"/uaNCh8fyN14DsYYGBTkAjz6ZgQ5.png\",\"w.\":\"/iNsZSUpyQbvRmCY1JYvi3JZ6yQh.png\",\"v live\":\"/xlc6O9RcjItzvzYtxl6tCYMol1t.png\",\"keshet 12\":\"/guJs28rlEaZTKllD5J1qD7WRQWP.png\",\"paramount comedy 1\":\"/KbcFmyKSpMDMIsswtQzi7aVNZt.png\",\"shanghai television\":\"/mutdWEoQEtand10DBc19dbI6hWG.png\",\"motortrend\":\"/2BGpHwd0CpeH4fKN32O217RbtI8.png\",\"jetix\":\"/et3ZqCUvzqGVFIs6OiPlw2rxRbc.png\",\"fox kids\":\"/rUb71E91POVCxpXGJ49pf6m8Ukk.png\",\"tfx\":\"/mTljUaXNXAlVSD8aGcrlQPpmdF3.png\",\"brat\":\"/8zD7H0aJH9YEISk1OyFuLPpFu91.png\",\"viasat3\":\"/w6MMMIa10yZowA18lLMKMfMZ5d4.png\",\"kbs drama\":\"/6IJTwLUuzGVKwICJBC7RMgNm0Zq.png\",\"bsn\":\"/34sNEcsgcTAuDidRMrgxdfNyVjL.png\",\"tys\":\"/b4Ivxqz6UY0EogjYc09Y22ctC6F.png\",\"hot entertainment\":\"/crZefYhlhwf4q1bp6qzDUPlotIi.png\",\"cctv-14\":\"/pb8Fon4VjQ7BpGcthzjOqBIzyfW.png\",\"hrt 1\":\"/i1rGoIBvxpP9wNYrjsELctDfCib.png\",\"hrt 2\":\"/yToiGhuIZfIbcnrgxFBjSFTiYpZ.png\",\"uktv style\":\"/5pm09nDmkt0Yv2eIJJIHaKUJnW0.png\",\"goltv\":\"/2yFWfYPux3l6WdDDvqNuo4cBzo1.png\",\"ecuavisa\":\"/9LYOSf9psHLY9DsB2AvfECtAzMG.png\",\"etb 1\":\"/vVZ6oBrw4cxGEBv6pmKnwVPcpt.png\",\"ert1\":\"/NYry3kLLITgJptE1pkeU52jRZl.png\",\"tochigi tv\":\"/2v82HpzoKRveJEJcGHkeTcktMRM.png\",\"tvtropolis\":\"/hZ4UqGsabYK356avLp8rrvRxny5.png\",\"discovery home\":\"/nVAkhCXG3I4W4Nk5kGhA57BFSWa.png\",\"uktv history\":\"/1oGs72mYB4gp10gmIMZeG3Sb5Fd.png\",\"américa tv\":\"/rLnMBF8MVSeQr975Btb5zLQjP4a.png\",\"universo\":\"/e2uRuY5VobEg67VLfow0Cq765o5.png\",\"jtbc4\":\"/8ik7JYVYjKH6ybX0DGKB9Gbj9j6.png\",\"abc nyheter\":\"/3onReYvZuqcf8w7WNKMjTjQO69u.png\",\"canal q\":\"/bXJlpIwmXjWmTFDmQKWKFZpOyXm.png\",\"start\":\"/oOin3L9AhYzQzKL1qDrYNbePzKE.png\",\"3net\":\"/3NWCn4m29tjInw1QcwiJaI8b3XX.png\",\"belgische radio en televisie\":\"/lpjHtqZkBZflKjCe33nHXKF7YNV.png\",\"milkshake!\":\"/wsFJkUNuvuxd8s3XYDSDxh4HDOw.png\",\"mbc m\":\"/5u9vcoUlsBo2M3rxjOnfNhXXB2E.png\",\"fifth channel\":\"/1u6wDJazKUSS39RaDmAgZFMsIpC.png\",\"perets\":\"/xelYsoekbDNZ8Bh6VEWwzSa6cXj.png\",\"che\":\"/Ls0C5phLTXp1dYpHlXRm9gRiRE.png\",\"360°\":\"/bca3aMuF0v3hgRFKWhZ3qcsDUsM.png\",\"directv now\":\"/6Gsz4JrWMVvx5OPepEO4Bc5zPsn.png\",\"tiji\":\"/bSSdpKi6hPwf06pNu5TIUymope0.png\",\"3+\":\"/lCkMbSafIjIAj4FYGilRLM765Li.png\",\"cbs reality\":\"/nXC4er1ccpuWVrI2cQNHjuQqAF2.png\",\"voot\":\"/l6RWXnmlxVgMsu2G3Y938DQszee.png\",\"foxtelecolombia\":\"/qQGutAt6YQYRAwWc9ZxHQe3b00e.png\",\"russian detective\":\"/kXb9FRf9pRInsyFCuXSeeQk8pfk.png\",\"infomix\":\"/jhWfbeJtP992xsG5MbeBigXtDpL.png\",\"art hekayat\":\"/9fH1kXMwRhXciiEyF7fFGX7o2cf.png\",\"art hekayat kaman\":\"/At6Y06E0eKvIsMACfuGZVuy0c9J.png\",\"apple tv+\":\"/4KAy34EHvRM25Ih8wb82AuGU7zJ.png\",\"mbc 4\":\"/zT01qgrQAqmLRjyPZ3otBhaYNMQ.png\",\"dazn\":\"/kGipWQCpZcafdYmF0NICQmKw0uQ.png\",\"srf zwei\":\"/3OcAkffT1ZJAkWPQZKTDdKp8wmC.png\",\"super3\":\"/43vUOZkb5nBAYvYPj7haOeczoqU.png\",\"genius kitchen\":\"/2kPJ4CjpyZqDvZYwYnb3lrb0etz.png\",\"be tv\":\"/fAZfGwB4j4fTxZu44XvxUEDcwmU.png\",\"smithsonian earth\":\"/9dgMIZKjZWCWxDkqj3KX8KFxRoq.png\",\"asianet\":\"/jwCbVQKnETdndtnkUVF1xBIZwn.png\",\"neox\":\"/2OOKfHMOMqN4l2QU5WLRjwp47iz.png\",\"colors kannada\":\"/bt7SKpvQmDxnqKnJIhPhxOCPUCf.png\",\"colors super\":\"/lU82n8N9gwTc3AhPOpXrssvzODu.png\",\"colors marathi\":\"/wjGZg6wjVs2XIz6b0ihbai9yM3d.png\",\"colors bangla\":\"/5Bj8Mtk5rQnBwcKKb1lWSkeqzTd.png\",\"tv-6\":\"/6ErJUdDHOHD0AE8Ae2tdmPkOhTO.png\",\"oksusu\":\"/xIHkp7frQMMTlXKEAUtDQUZgDa1.png\",\"teletica\":\"/rd6CM8f887MW9MMkRyuuKIpMH6K.png\",\"odyssey network\":\"/wyfqylHYxhnTMXVZTcpdZrvtpL8.png\",\"zee5\":\"/cakduIXxOWnOasbKSMZ6Xvw5REG.png\",\"hbo family\":\"/asKFyxwgYp8WluQkWhGFYj6IYYd.png\",\"yle areena\":\"/vxp8LTTXwGFrzNM7zSgzl4DfSad.png\",\"rivit tv\":\"/cCyMbuRim1x6WmnbYgCFn2AVm7i.png\",\"energy\":\"/b7cMzGGOxz77OIwIDqZrSyGuJMP.png\",\"azteca 7\":\"/hRxYQTVSCRh4uCs3Wk4AisHOKUI.png\",\"anime zone\":\"/vmlXZtGw76HkFrXR5UqvEklpCy4.png\",\"česká televize\":\"/x3BUIowxU2skXOpnOeOkl7kIYDo.png\",\"ntv mir\":\"/oPqmdzTb2VmZdpjit8HV4bvbIT7.png\",\"7tv\":\"/7FgVHt1k6lEQDMkvrCVvDbswjqn.png\",\"cbs.com\":\"/fpkDVuVhNCDDh1JX5ikWGcyBx9Y.png\",\"passionflix\":\"/zXeUVqLEoCFeiUHb3cDSrNWz0a.png\",\"vt4\":\"/fueFFOI4BXl1OMiISPMRLxcdbn8.png\",\"q2\":\"/3uhuCtWhe4tVc1TfiUq0QuE6GT9.png\",\"crypt tv\":\"/ydUEoQoCFfLvj8KFoLh5AC343Sb.png\",\"discovery kids (br)\":\"/8woBOtitimA6diobq7sxCIwj37G.png\",\"omni\":\"/lGE7lnRo1zmxbdo8hCFxhr5WuJU.png\",\"joodse omroep\":\"/2O22eqwTVXviivKCiwLJyut7XhX.png\",\"gulli\":\"/4b8ZM73YKqFCJLbQB70Pyw5wQCK.png\",\"onf/nfb\":\"/dPdaLDtWPiuJzXP1CuNUxxV0DSU.png\",\"sony liv\":\"/8FhUdXfYC3E2EfntzwCdHYvo4vt.png\",\"fox action movies\":\"/zltsrFgk824Oe4oWNmvKyya2EtB.png\",\"jewelry television\":\"/e9JDcchf0AKjBOXnkClvizhm8Ba.png\",\"fight network\":\"/7Ht7PCwpRkYyoOOxVlQPJUa0QFl.png\",\"hkstv\":\"/Ar1dX8uhWORSAV6vkKVQM1EoaLb.png\",\"rmc sport\":\"/q38SfscVUlRaXW1l6w0G7vkdBlG.png\",\"cctv-5\":\"/zYZ87Aa4549mSAzanVZqgcmgVOU.png\",\"mono29\":\"/43zGTXOvLAIqIfuJYHhMUFxG7i8.png\",\"n24\":\"/1dmD88GE8Y5YRtVdhy9yI3pxovK.png\",\"massengeschmack.tv\":\"/jZFkmHcuSDYxECLeOLSFSQY2jHZ.png\",\"sky atlantic \":\"/iVij5p9Mxfoj7mOmcLvVlKgIx9p.png\",\"hello sunshine\":\"/b94sYzp4YUqi3DqIUnWnfrmxlN.png\",\"contar\":\"/bLcnvFY4lGJYoWWEc2qpAAkgewp.png\",\"canal 22\":\"/cIbIoO5hoRSRAhrUYOeYCRK4Zg3.png\",\"ondirecttv\":\"/uPNX6Cikdj4ES3TkyOKA9yedDxP.png\",\"canal fox\":\"/1DSpHrWyOORkL9N2QHX7Adt31mQ.png\",\"sky witness\":\"/mGbOeG43ckHhAAsK391NyLyndoK.png\",\"discovery channel \":\"/vWHt63dgArLtqnsSTu744G6g3sS.png\",\"kvcw\":\"/6NO3G5vuhkewHTuUAoZpjTxcPFh.png\",\"hooq\":\"/s8QZ9FywliUa3FQdjDGU2hopcfi.png\",\"acorn tv\":\"/vKbnLMCglPQch0OPeN4KpPjvNHW.png\",\"umc\":\"/4nGQWWLyDbQpJvUbXYlZKHFjUFP.png\",\"spiegel geschichte\":\"/3Urhc0dBh4jKL8InhwsjY2xRL3E.png\",\"studio 4\":\"/eUwMEnQJlXBNBXydcsBUJSqKVuN.png\",\"yes tv\":\"/o36tNm5Scu1mOltjUxUdSbKmNCl.png\",\"ruutu\":\"/ieV9u8Q0WqTvlRcR3TTceA1X2r5.png\",\"style network\":\"/hmm0xFl7nydC8VR7YdTQ0TXowQ6.png\",\"eros now\":\"/4fbNjuFBZ9OnG7keYr3xJFYG5hi.png\",\"aj+ arabi\":\"/qatnFtgbatXo5TqY2P7Sx5y6wvK.png\",\"rds info\":\"/dqHb8f2Fdi5fpzysHIKh5SCRiw9.png\",\"chukyo tv\":\"/KaIK93kJFD8WKH451owMdmNOSz.png\",\"rnb\":\"/kwC7bfbGknKuL7uxKahk6paamlp.png\",\"ustream\":\"/pRc6dPI8TIaZ61bF24bh6Zwk6QS.png\",\"qatar tv\":\"/5SM3cwPsh5uqb3YKAQWVzn2FGcz.png\",\"sat.1 gold\":\"/wYrKXhcChgIqfDRONnANmOoFBm.png\",\"nps\":\"/bbHC6qV98IoKn3veSx8wDiOjtCi.png\",\"nos\":\"/982W7v4h7zS9yO4VZIjHw1ORCl.png\",\"disney+\":\"/gJ8VX6JSu3ciXHuC2dDGAo2lvwM.png\",\"focus\":\"/3mbNzHGZJoIluOgebfCgwsQcuU0.png\",\"rang\":\"/A04tNYZI1mi0jxFCsSlKrUPmGeQ.png\",\"kakao tv\":\"/uZpVywEEp1FaMAWZKQD0ZSa9nel.png\",\"freesports\":\"/1xCjgcYsJ4DaD3NHzDl5bNuay05.png\",\"sky travel\":\"/m0oaYs13Y3K2vsqoxcWYjBBpDyM.png\",\"tro\":\"/4sPm8L8wX1gp3iBXqU5bhGqdYvQ.png\",\"fod\":\"/xMGdd8hvu91BK098UwbUv9IdjM5.png\",\"gusto\":\"/xhoPVvSmHRm1984Sd8GWmEQ6YQa.png\",\"kbs w\":\"/ouocJy6EukDqdAIS5tqER5uzmIX.png\",\"man-ga\":\"/v8WRVyDAQ7xA7Lt7jTaz5AFpH3z.png\",\"tvn 7\":\"/yaevuAiWmYoYyoa9N2uYyDVRPtz.png\",\"love nature\":\"/sQzxQ1GOyNyxyaznugkicB1JNDO.png\",\"n-tv\":\"/aW9VAddPX7Z1GNCOAXzXtaFsYSa.png\",\"abcd\":\"/qFL0BoydPB60mO22edlV3RfhLUT.png\",\"deejay tv\":\"/xkRLUdy3thCCST8gkVwdJnP4Cfm.png\",\"snapchat\":\"/yQcvQqcOWxprmIuzjnQEZfKjj0G.png\",\"rocket beans tv\":\"/iH3nusZNkYn8lXPCZHKCS4pXrlc.png\",\"canal savoir\":\"/z3JFzSXjwgzGuCCynk2nifjQ5qQ.png\",\"dkiss\":\"/70ezozaVCQmchziAM8HgBFmT9Qp.png\",\"wow presents plus\":\"/29fbulsOz7d9JDJLjiNkxq0jS3Y.png\",\"rocketjump\":\"/7w2U3oxMYybsd6lBtcJEiT3zcII.png\",\"eurêka !\":\"/uoYFG9jVdyA7OctqW1WZdSE3pnX.png\",\"netvideo\":\"/kNTluzyx84RrGizI5zh03lwADmD.png\",\"gyao!\":\"/e4Zb74V62MpQRgtgZnBeGEt4UE9.png\",\"hungama\":\"/znFJryX0SQflQHmJcgbWkuvWN5N.png\",\"tlc (hu)\":\"/i3Qh6pjy2DZqxAXcuOGXNTyHl2v.png\",\"toei tokusatsu fan club\":\"/8jJMPoqXIBdcWKlGFTrkGi0Zyj2.png\",\"rtl+\":\"/2joN5UqxLRyrE8JJJUnbcvqU4hm.png\",\"squat\":\"/inl7RihwWZmBS7tGPNMCA9dMCUO.png\",\"kanal 6\":\"/2IbmlZYWPDCAvSXLrYxKsFfMwwx.png\",\"hallmark movies now\":\"/zjgwf8ZDrMf7zxhe8WdwzPFKnjc.png\",\"abc kids\":\"/efIOJEqy89zWpK4fDlOXwXtTbkE.png\",\"sjónvarp símans\":\"/dhHde76oIEfmqjAuvlkxlGKWYGK.png\",\"tnt comedy\":\"/w0ZtygaHGHBxLmdS2ZqNDDYyb0w.png\",\"premier\":\"/sE6SzJwo3yDXntlBohmHNGxmevq.png\",\"hub vv drama\":\"/dlSncZ9PmlvCTGOC56hX05LdQ8G.png\",\"gyeongin tv\":\"/bjdegNjZyWFcCIAxyA13o2FkrzL.png\",\"vtv \":\"/rqLpVk7zzDFUT0BFOIEVbC6XvEP.png\",\"tv3 latvia\":\"/uxOAZGDIoP5ZRGSw9QX9TxNpLgk.png\",\"pursuit channel\":\"/jgaDTPtdlNIz7L0X8UuyB6WNE5J.png\",\"spectrum\":\"/ta0tkJ6VUugXwQzdea6T7jzUYkI.png\",\"the fantasy network\":\"/xlRy985kuvtyLXurGkudazmgYYw.png\",\"safari tv\":\"/1qRHeqcNJQzvzdaeAh2NBahs0zk.png\",\"yoopa\":\"/5TMxGA733YHkPFXIA8uxs5VxN2Z.png\",\"makeful\":\"/44K8Iq6d382bQvP15bgGoRZ3bjO.png\",\"top channel \":\"/aQa3PvbykKhL3WWTCovE8KOrCpo.png\",\"food network polska\":\"/jhxxYJx97vqPiGL6rZHDVixJpfN.png\",\"ullu\":\"/plFwH8j6fGrDE2wJ5rYyqJ2046N.png\",\"zeste\":\"/a0J7SLebqWlpVDr1lJwW6uAwVOF.png\",\"open beyond tv\":\"/qH5M9dRWAwpPJ34PehcOudqh2hq.png\",\"family gekijo\":\"/uYiTfGXmZnCzGNCB59QcaZmnffT.png\",\"imdb freedive\":\"/AqLQUhGYbgIalivsos4IeM3RIPR.png\",\"télésud\":\"/6bLMsJU521MWNT6wgLt6oaMIvP7.png\",\"tv5monde\":\"/b6KDy3HbsO4RpnDPtHDSfsRq90c.png\",\"russia-kultura\":\"/jN9V5mPoi7nXNr2y97v9vJXLIj.png\",\"tvnow\":\"/fXCqxsW0QhZTTfL5YxDuDeKNvxJ.png\",\"nrk p3\":\"/nVIPfpdHtgy2SSczqsv39EiiC6Z.png\",\"teleuv\":\"/zxwuX0eT489Od5KkPGIV9nScR6g.png\",\"now 26\":\"/zYJkN0FO4WclBYu6GWWB9fgeHJm.png\",\"workpoint tv\":\"/luK3LME1iDphfp4r7Te00PIi2AH.png\",\"super tv2\":\"/9Z6XVfyFRFneRhrIPUfX9QZjGHz.png\",\"shudder\":\"/Ap2jtXPy3QlLMi1GYLtasq7VoN3.png\",\"matv\":\"/iZepWlKX8Pj0MYjfxyF4EHQcLez.png\",\"tvi24\":\"/p0LHKVYtJaPi0SyAS34ubx0eTtC.png\",\"mx player\":\"/xKMlXKggJ1mTsfC5OSnlBKuFccO.png\",\"bon appétit\":\"/8crroBYIfllpwrM2Fsuce8XZe2k.png\",\"kan 11\":\"/hcG3NWwfOP7lW9tgxcWwDovEdT1.png\",\"mtmad\":\"/zH1XEtQleT7o6cnfBmePj0xiJBY.png\",\"animax korea\":\"/aA74agiTaAoFrGmOfSDyBlpZocE.png\",\"aniplus\":\"/1RVv3b0CO9YHf9PN1DEih3CDUwz.png\",\"viu\":\"/yEv7NZDYYxWak9cytEKzjO4Te1L.png\",\"cbc gem\":\"/m4nAbyBNzPHitupGll2n9WR5kp8.png\",\"france tv slash\":\"/6MNsYgexPfbWm7pxfosNqltwVDa.png\",\"france ô\":\"/y6b6Vc4Jwn4zHrwyYa99I4ODUCc.png\",\"comedy central (au)\":\"/waWpWvfVRCCFUt295SEMkebc0ai.png\",\"playhouse disney\":\"/4v1r0aXZkoTTIi8MJcNrvTaIG0g.png\",\"machinima\":\"/pjiP4WqBZm90oYHK4vYrzl64dRr.png\",\"clan\":\"/lVYNNXEJhUAoCxBl0cJ9KvsIjRs.png\",\"mall.tv\":\"/7IkEe8QvysSJyMZplqrliiUfqE4.png\",\"imdb\":\"/hcypVQGTkMikAgJlpasssIohf0w.png\",\"azərbaycan televiziyası\":\"/7BzkJB5PwrVWJY8T9yw1tKk4El8.png\",\"mewatch\":\"/p0kcc0CINYJIKD3hDkaW1vejCAg.png\",\"idnes.tv\":\"/buZupHGVRcmtpa5j6mvnJKzOhhj.png\",\"iwant tfc\":\"/C9LDkqA2vosa6ibxue9gZrCaQr.png\",\"claro video\":\"/mqWLsBYQmQbIuLbHLnrQW7LXUNF.png\",\"friday!\":\"/bFrRqO2SCc8UXXpraOfqSgsbVQk.png\",\"metv\":\"/qrqFyJGAxOJvKXP2y2dQss5XDkW.png\",\"mbc 2\":\"/fDqP1EkqM0Bobpj3H3bXj3b8ZAt.png\",\"otv\":\"/qQGOqPmNuhKUgWHsVulcvU1rP2z.png\",\"env\":\"/9QVkKvpaetT4SQOBu2pD6Q5Qis8.png\",\"planète+ a&e\":\"/h8WAnO9OIO0k2hLyKxTbXplYFUz.png\",\"metro\":\"/mfeKu8D3mrR5efzkZ0Hq70ww5kO.png\",\"bell fibe tv1\":\"/xJuu166ou5sK95UK2zzMkTkN6Jf.png\",\"estrella tv\":\"/iGw7s1jiRcWggyURxSHfv2DgIIE.png\",\"tvn style\":\"/bd7eZpipCRGTjLtbQeYzlo93h4s.png\",\"canal+ discovery\":\"/uAfVBVVmDQIEVRGZQGzb0X3X5hX.png\",\"polsat play\":\"/6hJkIbD5xRfNMvHbluo4zKmVUJR.png\",\"drink tv\":\"/mU3bZsaS8bX08azWJJIQFCDRcch.png\",\"ttv\":\"/jyl5bWGbhSJ3ZnMcvZAJwUs5chp.png\",\"señal colombia\":\"/w0rERIXkvfI01ey6kDv0H3QQTWt.png\",\"rai yoyo\":\"/6tIBzDghJkH9hFntmMKYevW04cK.png\",\"quibi\":\"/aYl3rkrvZZZUtv7JnKNbJeWFwdp.png\",\"kanal 7\":\"/j5sCHmg2vY1sUmgOftAVr7HsuHa.png\",\"bioscope\":\"/jy8ZINiQHxbLCHatbnxxmD23L5T.png\",\"amrita tv\":\"/6dhn5HQprA0S3xDvXZWW1MPCOBU.png\",\"lcn\":\"/io8XzF0aChBUCPr881njwSZxt6m.png\",\"nippon news network\":\"/m8ABeozh6TciFlSLpYOaK0vZlWs.png\",\"sari-sari channel\":\"/wKwaz7ToYijKepNGUl7SlQ6bnYt.png\",\"cctv-10\":\"/ytV0qzKx3o5sdtM0tqLXBWEugEb.png\",\"canal 1\":\"/dVoNhpCtTKW1Tr7sxngUx9bshys.png\",\"o tvn\":\"/hRUFZpGepmxZ0LCdgpb1uISz4nw.png\",\"zeus network\":\"/8BVAYP3Y61LvjNZr9mcIr1IBSoH.png\",\"joyn\":\"/2OWbACUYTojW3CSzbEuPicnAabp.png\",\"sony espn\":\"/4JjhUXUKpnk7ALQJe5nPbFdn5Tf.png\",\"hum sitaray\":\"/cqqwSxneFMDBcqjBnJ3OfKstvNH.png\",\"xee\":\"/2LUK4SQdjmQ6rTnQyQzbhT9evh3.png\",\"trece\":\"/fh32Yocc1650C20U9KNudtxIssj.png\",\"trt 2\":\"/axA7x7TBJgN5WHjmGoEmQorOq8u.png\",\"hbo max\":\"/nmU0UMDJB3dRRQSTUqawzF2Od1a.png\",\"itv encore\":\"/ucMo8oAZCwmn5IFjUV9yDnWpC2O.png\",\"100 tv\":\"/xWb0SVMUndHG3UQCrMOFt3aTiG1.png\",\"foro tv\":\"/oX35kX14EpjkEluiYE28FF3drE0.png\",\"tv hokkaido\":\"/g1jerEP7O2rIYd83AaLKXeZlbpI.png\",\"build series\":\"/ZJ7pobVIemTKkpRPgDJDLBGCau.png\",\"zee zindagi\":\"/8DNSIug88tDYzxWuNBQwbAt6Sw8.png\",\"viva\":\"/g0Uf0hblsRggoZBPwYBrKkFj8do.png\",\"bs4\":\"/naTpOY8AjRtgBw0JXafrhAD8ZBT.png\",\"azul televisión\":\"/f9LUy0pcsI3DfEWc28wgHs7NBEv.png\",\"pluto tv\":\"/6xI75dFULiEks0Dqm3Uag7CiC29.png\",\"namava\":\"/uLOLNOsN5xMvbB6ppk4Dax7uzhS.png\",\"gagaoolala\":\"/7pXbP8D3T3BfXKyJpbalc96lFll.png\",\"animax asia\":\"/gNsvkmJY7QzRQ7AZ0QK9PeONIm2.png\",\"canalplay\":\"/ij9WCzOHq8oPo1nHnIISqdkFcPo.png\",\"la cinq\":\"/vZDDmxWjogmw3sdpiTQhaiZdOmW.png\",\"ab1\":\"/9uwYyKu45dv5DyZEuEp2JXzy6Cx.png\",\"ab4\":\"/kB986fmXXXfNtpxJ7fbIfd5JmKs.png\",\"bbc scotland\":\"/pkei1FCLR4Eox16BrN6sGL5FEfL.png\",\"dr ramasjang\":\"/i46iC26w4Yt1Q4JUK9RtjYLDt2M.png\",\"amarin tv\":\"/x7mp7VgOFS1LqFvwZJ5OXcB8xLe.png\",\"prosieben maxx\":\"/eQPoTcjYtZocXyv1RFFWZJ3JKZ9.png\",\"pantaya\":\"/ytTHs0Mw26jCFQtRUqdajqR5F13.png\",\"globoplay\":\"/vMj2Q30VxvNt0VAIQzb8ZQHWwNZ.png\",\"cine.ar\":\"/cLd8MF4WjSNENPYVHTPbfEfWRXH.png\",\"investigation\":\"/ewPhtyNWEN4Jl6kgp42knjAxRxK.png\",\"azteca uno\":\"/yJyGhrAaFdmOHjiSjinNjIcErHs.png\",\"ami-tv\":\"/hEdPOGY7TwSRZqvsEswM6BzhzfS.png\",\"funnyordie.com\":\"/sBzU4PjJ8vH9YGUbfFnXs0sDHuM.png\",\"astro ceria\":\"/iY099ZpmP1j5t9QkE2Q3bbBcsHT.png\",\"jstv\":\"/nhVwbUQM3WIbZdf2Px6AjN1m981.png\",\"tps jeunesse\":\"/8XnIxN8hFMCjfwssg6HM88Wh1g9.png\",\"vudu\":\"/xsiFrVU7PirDYY0av8SiyswYzLf.png\",\"toonami\":\"/5BHAWTdsmpCcgNQxUkOATOn6iNV.png\",\"bet+\":\"/vEBCYgIAVklR4lg0ev9bASLzE6h.png\",\"redlight tv\":\"/mnXLCetNP9oEZx1KnvkL7etj0o0.png\",\"peacock\":\"/gIAcGTjKKr0KOHL5s4O36roJ8p7.png\",\"wavve\":\"/5x28VbYCOuLfMRePMDOcax94SD9.png\",\"pts taigi\":\"/3mRQw6lQrZIaOu8aYR1VAUMjULk.png\",\"stuff.co.nz\":\"/qx7huXKL5kYg1dXuDxcOA7BXFZO.png\",\"svt play\":\"/xtXMBjBC6YhQzODw0XWERsHvhdz.png\",\"insight tv\":\"/cysCdp10yByvUrvRoJxNUtdMFhr.png\",\"čt art\":\"/pbZg2N5BVb2Hc5cpJIYjbuqWCDj.png\",\"čt :d\":\"/jr6QJwDKiEV2flOJnv2bkyfIhdn.png\",\"televize seznam\":\"/ePO2Wcv5B3zw5eJgUxNp0CJHpIf.png\",\"čt24\":\"/27GU3PdUPXwsX5TqoGKCraPJj6T.png\",\"hakka tv\":\"/lanjDqaaJhNFjHLLqBb691P6taR.png\",\"prima zoom\":\"/gW4dPB7TFHP1AGnL9bvhQnRWaj5.png\",\"universal tv\":\"/bKzvRKx4kTWdJLl3trEc3p7cldt.png\",\"hikari tv\":\"/1hWkGw1Srw74dFzIYDgssn6OoR6.png\",\"hikari tv channel+\":\"/pywXRLxb6ZuXxZzMugQCAKN57ab.png\",\"سورية دراما\":\"/iTTCHsQr9flXKcvyDTxZ4cbHept.png\",\"prima cool\":\"/erohPAehVomIV3pglLuDquAQsCo.png\",\"prima love\":\"/753SEOKvGzRE18mfuCz0cr8LStW.png\",\"çufo (al)\":\"/4EvKLwhdagu2KfhicZt2KbkOpu.png\",\"הופ!\":\"/a1TGWWQUFBNdqTV1XKpa6GGkJJO.png\",\"first channel\":\"/swp6U9KaEEry2NwZXFswBErRAeA.png\",\"腾讯视频\":\"/kiDJEB9eyV0syoyF51zjM4ySjpI.png\",\"hallmark drama\":\"/bVFOBsvRyN3cJ1oEhGu7BqPZwGq.png\",\"tfo\":\"/8FWZNgPWEazdwA54aul7bKOvFnq.png\",\"الشرقية\":\"/wivcWv2xjte9iRqvZxHYOKGczUk.png\",\"rai storia\":\"/7ypIuH80IDEZwScONIjrBqihXVU.png\",\"bayam\":\"/nykmIgHsU6aQrmuNJc0lSxzz2qz.png\",\"virgin media one\":\"/pPfZlk1P3zfNyIMginkstYPn9wC.png\",\"raiplay\":\"/xuijJ556OSE9xqw9C6Y8nf3liKP.png\",\"comic-con hq\":\"/5AbiaYEKlsY0qFjWsdC7qxdSsFQ.png\",\" 10 play\":\"/4Z1e4FlcFhak6BoKE13EckL8fmF.png\",\"bs tv tokyo\":\"/lUEv1JU8b5UcZJ1XoJkAq8o6UVd.png\",\"dumpert\":\"/mglYZoTvCPemKgrpRtJXney78x0.png\",\"junior tv\":\"/gqeFiLLY4RhN8Jw0WlcM04LyOxT.png\",\"rds\":\"/fvLrKSvF9uhQeCFKJKhUCkyPXmU.png\",\"tnt españa\":\"/fwt8nHdUAN655lIWC1RButfsiY2.png\",\"latvijas televizija\":\"/4cAeVQcPBxttZC9Y9FQItgFKBXU.png\",\"novyi kanal\":\"/hy4KtbiU5oUWL5JjmvYy3dmyJLb.png\",\"la trois\":\"/dRvulHecDfIgJNBA4Dyltcpaqm5.png\",\"svt barn\":\"/ck9dgW7WZTTYmcTsXuR2MIzihtv.png\",\"alibi\":\"/goAPipwx4tSALdM9kJZ7ovGjEZg.png\",\"diyanet tv\":\"/ir108f6LYvYMaEfGJqVVva0xbiR.png\",\"cottage life\":\"/dfZGwt6pF1IczG4nWIOM3jXB5Ga.png\",\"la5\":\"/ifDMYB7mJ0VkbvNaP2SW7mIbtzD.png\",\"arte creative\":\"/80vsDtw538sEbfeMZayWqxzbB9D.png\",\"ici tou.tv extra\":\"/wP7QDZA7TOCa3B5rD00DtBjI31.png\",\"tello\":\"/97OWtUjBFLxtztMrplqrOhYtMzD.png\",\"2stv\":\"/azCNFEbDIIIeJ8yM5hDKQtTKH6X.png\",\"sen tv\":\"/sUisp9lJUDI7dA7SP0X7wdJYsZX.png\",\"wido\":\"/dkcFYUXomfR3HhZcEeH6eWXPY1E.png\",\"club rtl\":\"/7tjZK7sd7P7zz6fthAl2ELN93vU.png\",\"jtbc3 fox sports\":\"/15puhQImE5hGj1lxXG4IUdN64im.png\",\"comedy central netherlands\":\"/6ooPjtXufjsoskdJqj6pxuvHEno.png\",\"vidio.com\":\"/eYdP5Uikvq9K2x0KK1emIx2WJdJ.png\",\"kan educational\":\"/3R5OK6NxfFUnumjCkDCs5yxc4EV.png\",\"à punt\":\"/iKEJEInEYPYDfQIZVGfEFVterDJ.png\",\"5-tv\":\"/nIey7JqAgMmH0UC1ZXuTawGdAHL.png\",\"discovery science\":\"/afxnsEk7jsfQlVbK0scU6gcuUeu.png\",\"tv barrandov\":\"/vQde5X0ChsnMA0jNenBgtj2gTwW.png\",\"nrj 12\":\"/uC0JP1tx3FPNTb8B0prVgRCscdf.png\",\"rtv slovenija\":\"/yD8yjPqs10C0ayb38hKMXpQonJ3.png\",\"wakanim\":\"/mBVrs71WfQiivUPrRQKvzAaXlb6.png\",\"tvnz duke\":\"/tzwUoHIMWKhQzBw6sytWK6faCjv.png\",\"net tv\":\"/yBzinXzZCmHWmK233JEX54xPdL5.png\",\"hbo brasil\":\"/tuomPhY2UtuPTqqFnKMVHvSb724.png\",\"tvn movies\":\"/kIxFo8qXuqVEVra2EKeuJGlPGBO.png\",\"nagoya tv\":\"/6NBvugZ46HATVS6nXmWaLBHAtE8.png\",\"hidive\":\"/rTjC2AeaaSTPjIh9YIqkPzVWpUw.png\",\"puls 4\":\"/fs3NkllP0yllYAOOFcIBWKxLmtx.png\",\"fct\":\"/1UBs9MeAJZyt8LzHowk25cXP7Ez.png\",\"sky crime\":\"/j1DtfhlOytvbV6QVnrZHT3xBkAP.png\",\"gunma tv\":\"/oe9SS3K7nqf4s1VJpbqWx6wlxBb.png\",\"hbc\":\"/n77lM12X5dhGHb0I57RmbtqxFsd.png\",\"ktv\":\"/pblprS2dwTv9p69RGJkscm5365o.png\",\"tv5unis.ca\":\"/6Q0QyebAHKzDLHqW3xzajNYnVPA.png\",\"aptn\":\"/d5oq1JLxnwRJ8YWCmOnr12iLIqA.png\",\"rmc story\":\"/uKuCdsGlB3QETtIqoReAkEFS8am.png\",\"d anime store\":\"/xm9X7UUrhOcsRC6o3RZtrNvaqG8.png\",\"sapporo television broadcasting\":\"/qV2FvarOatWAtBGk7tLe2KuQMyn.png\",\"weverse\":\"/cIpoMkLXqYUPQgyESTbVcnPivh5.png\",\"rkk kumamoto broadcasting\":\"/vSn3Au44pqNvrRAtOE7CjBWOOdz.png\",\"vice tv\":\"/iA3HkJ8eNQZngQAjETO24YV8p1n.png\",\"hokkaido cultural broadcasting\":\"/uKTzwez1p8tB4dnHMCeDjdN7xlR.png\",\"ab3\":\"/yZdt7s80AWA1aqM8OsP6rD4CfMA.png\",\"wetv\":\"/lf9ih4TfFC845idJHjuggJhQNL.png\",\"television nishinippon corporation\":\"/6HPq1wlbqjCDa93OgaYp7E0ANIM.png\",\"kitanihon broadcasting\":\"/kGUilAgTPGgaALslFvjyGvn4TKj.png\",\"tps star\":\"/d7mFDlWCM6dvNah1Dn5q3haTq8k.png\",\"atresplayer premium\":\"/tRcLVDie5E5dIIRvA8Ojjuvlp7k.png\",\"yomiuri telecasting corporation\":\"/wWiz0zwFX6WOXQSwsPyTCKITNQK.png\",\"rakuten tv\":\"/4OMk9wkAMwjcVM905BDyDC3iIgC.png\",\"anime houdai\":\"/zQbXySts9Nz2wus72JOqUPWG3Qt.png\",\"anitele\":\"/6DTnriulEzrdURGjPV4QbY7WPuJ.png\",\"video market\":\"/gx4yubRa2NkJ2Amia2kiNeQ2cja.png\",\"omroep max\":\"/9kyApMSkL89Dtmidc3r9IRtSfBT.png\",\"nagasaki culture telecasting corporation\":\"/qEIIHhehdHtTQ4fhuXrA29mbzmo.png\",\"aha\":\"/whVDNlpH9Z9gqU3v6m4o4stnjgX.png\",\"imago tv\":\"/jnTO547ERX26cOLTpKOSIPLquiD.png\",\"dailymotion\":\"/sjaEKT3ynyGDpOODdpLqsqJuy16.png\",\"rtl-tvi\":\"/1a0bsGJysuLK6iH0OaBnAfxeEB7.png\",\"kbs n sports\":\"/cfocxbV1M6E0NGEAhpLPhlio9g4.png\",\"rtl9\":\"/umOhb2Hp2MrBL4rYbYALlsdBY7j.png\",\"la sexta\":\"/AtJXlAoj0ITHKDN5EPJZPHJgLxI.png\",\"chubu-nippon broadcasting\":\"/gM0yhHNZxgJUUjb1o4abliDf0qN.png\",\"fem\":\"/bvjpiZxP8cCU8HYcHnC0wvCvs7x.png\",\"myvideo\":\"/q2n52WeF8xiBIA8efwtPXOyPT4V.png\",\"vijf\":\"/AgdI3DKRqx8wrXkPQAExHQSwmfh.png\",\"deutsche welle\":\"/wiZ9S6UFFGro47TnzgAR13IhneP.png\",\"distrito comedia (mx)\":\"/9qOOQy1cVCs84SE18ivIWpL8Mqc.png\",\"aragón tv\":\"/22uvDUdf6WfQr7jpSs34iKIUkUx.png\",\"kinopoisk hd\":\"/fyXApa9qRaYy9xUBlykh0iZBEF3.png\",\"rai play\":\"/3EOt7Yoe23FBVVAKbUPjMzxkzRD.png\",\"canal+ (telenor)\":\"/5rTyarrnWBpX1lOInrVRM4zXr5r.png\",\"téva\":\"/81QFkWQND1V5DQou9d65025qBTf.png\",\"el toro tv\":\"/ZYHWzIaxCjtzogQz2PYddxwXEv.png\",\"gemplex\":\"/m7Np4sh424iI6ApJrpb0tl49vc.png\",\"rti1\":\"/el1B87vLnyEbkjTxuyxjOpi8dkw.png\",\"france.tv\":\"/33mdJBGZzealAB1stOtWTCF0S5Q.png\",\"filimo\":\"/5bajjCwynBYseyzOUxOfbqpQy3t.png\",\"el terrat tv\":\"/th0Y7HsUwXvFOH8seRIzWk83OWe.png\",\"ひかりtv\":\"/4iCrWtLJbSHABWZX022VEjvftmb.png\",\"u-next\":\"/ydvZrfyYbBVXPDT1Tv2P6EmRmDZ.png\",\"okko\":\"/pMy0zJmwHTsmR2mxLd4ifkOSq6h.png\",\"čt sport\":\"/qB4QfmivJzhX6JLTfbB8BjuOD0K.png\",\"disney channel \":\"/rxhJszKVAvAZYTmF0vfRHfyo4Fb.png\",\"revry\":\"/dXmPubyyiZtgHXU5bSbSQNnZq6v.png\",\"snackabletv\":\"/mDiDmKmP4cWTfjuTSaHfVkOnOT4.png\",\"hbo españa\":\"/z59yULluBjVC1NzIGzxSPBEjr2K.png\",\"sama dubai tv\":\"/kbZ37FNUSIVFYhhMjGJV7X7AlJ0.png\",\"paravi\":\"/zAo9jDJJ9RRLOZ0ApH8xQcfJiRG.png\",\"more.tv\":\"/mrv0nhpnFIqc3ouMtHzSkH6lBn1.png\",\"el sótano\":\"/nJcGLqbapXY2dfLVbhfqB6LCLUf.png\",\"čt3\":\"/wCn4ZTUgH6Di5NXmpJ7dQNWhM09.png\",\"mbc 5\":\"/6y1eVaiCffvH7HOKSdEIJfIh8xq.png\",\"laugh out loud\":\"/9LiLRKyEokkUQPQTAKRv2wGezLc.png\",\"nippon television\":\"/bWiM2iY82B32NGmPCOFatgfekIR.png\",\"movistar+\":\"/8DdXvwYKBnLU4ZV9bOTOBAn7Zci.png\",\"6play\":\"/rPMeCuFqgCj4gNVLX75uEcFApHQ.png\",\"boing\":\"/iTubxZz2a5zjePACJYEJoQkIrJp.png\",\"disney+ hotstar\":\"/sFfLNkL0DZPbYlgmxu9aXp7TiHZ.png\",\"kapamilya channel\":\"/xal22lPb7A1c1FQoYdHzpOm37Kf.png\",\"ivi\":\"/97MWdMOnRccJvATuYw6wtMa4WJQ.png\",\"tvnz kidzone\":\"/1DzG1ozQW9zZ8HP7L2UiTT5h9zS.png\",\"tvnz6\":\"/gm9eU2DrGROjAkIoODCjnAHkOO2.png\",\"rai 5\":\"/mvh2XSuVFwksHTwaYNSr3umfAIR.png\",\"ten\":\"/8OeBWslnqoiEfBR9XpIOQrNgwI5.png\",\"ifilm\":\"/3hchCBmgzMuMbWoElH7y7YtCBUD.png\",\"friday影音\":\"/kP69k9W9KSCbtA4SRI3x2LmByAD.png\",\"nara television\":\"/9M08u5EOibJxZCWPG2kasrLrKAR.png\",\"channel 20\":\"/6h6b9RCqBJDCKFSrT2F7wjMp7uZ.png\",\"i-television\":\"/3t75XJS7vO7Ih6rJkIqUuZkePDp.png\",\"shizuoka broadcasting system\":\"/snEUE1ykwOwTAHvQIzKs9iYYQkO.png\",\"iwate broadcasting\":\"/zoFgqDe0v8svSA01yExlhcG1Bss.png\",\"broadcasting system of san-in\":\"/cQWQ5UZqFviyJNqvbuvgZUyly5b.png\",\"hokuriku broadcasting\":\"/wvBiZKeuACCz8kxeUJ8obHfHnqZ.png\",\"oita broadcasting system\":\"/iRxliwAcihUyTIjJbpxQ9zMGbEj.png\",\"tv-u fukushima\":\"/7jZrfXteN6dWampK5E6qJSScuZm.png\",\"tv-u yamagata\":\"/zOz9LK6WWLUWGuG0fthzDzCIXlA.png\",\"tohoku broadcasting\":\"/6oIE2LmtONObnw5wtFO4IIXwgkb.png\",\"mitele\":\"/2sMrOSRL5z5SneEjIvwRAXm8B1d.png\",\"wakayama telecasting\":\"/qBhdAhitNODSGDEmeK8p0XZ0JAG.png\",\"arre\":\"/6915j5uXxekwxJO6RFnqJMlMFc1.png\",\"bindass\":\"/3JOLGKlXiPII99E6sikyiUt140p.png\",\"nrt2\":\"/dttgimrdLJq9Tmkpov3RyPlrbQx.png\",\"jiocinema\":\"/yXrkdA9NlUfX2mRDhPgk0ye9frq.png\",\"britbox\":\"/n8YeTJow7qmgfjgoQ51Xi2MQezB.png\",\"shant tv\":\"/sTexVkysmuo2hjHzBG5YHn4TMCY.png\",\"azteca trece\":\"/tP07zSQEFmN84mfdYrPqdyxGDO0.png\",\"óčko\":\"/hvp4sRYLwe18yWagNcBkSVwnzwd.png\",\"13c\":\"/fWv4H0txKqviTx4aq5HxSFJ7j7J.png\",\"imdb tv\":\"/vizcQUuniStEN7Xm9IminQbuisT.png\",\"ct-1 (ussr)\":\"/usMoGOAxJ1YfYkPTwPTsDeVIvEJ.png\",\"nova s\":\"/ljhUSTgggthaCgI1q7gYoVvU2Fd.png\",\"steam\":\"/wWvkikegnLxnBEkVTVmjRSlUpwH.png\",\"tchelet tv\":\"/2O5ZRXQgH8yRvFCAXFyY2mw3OzP.png\",\"hero tv\":\"/uF8cYTk1EOlFO003DX4LVSQHVbp.png\",\"anime network\":\"/5Jc6AwN606Yg3efvFTQddaHrdFg.png\",\"rtk\":\"/5eolJ0d90bDHlgx0PTBIu2klH1I.png\",\"mts tv\":\"/t2ISbkv7CJHWkz2eLh9cGzFaZ1V.png\",\"vizion plus\":\"/o5oZOnIRbV3jxZusMOWkQLcnriK.png\",\"tva sports\":\"/a2hV9XrqawPM4fsg9CWoeTzW3Mc.png\",\"voyagevoyage.ca\":\"/pFzYb9kpO100rhN5kdx2zwT9sLk.png\",\"ctc media\":\"/8Sko4MFVPiwWInOuV1CsjHz0In1.png\",\"children channel\":\"/9XSgTaER2dC4CVQJ759SCazA2vf.png\",\"la fabrique culturelle\":\"/yq4KcjrdazO6YJhxuaS43iA7ltL.png\",\"تطبيق أكوام | لمشاهدة الافلام والمسلسلات مجانًا\":\"/bxUTlwXm2v1PDnVIkQj79Ewtdx5.png\",\"xbox live\":\"/4yYlTy9YoiJdSbxpsTJSKrLrSiU.png\",\"super\":\"/yVgt27IiNXdelp7B1BHj5IAWFfe.png\",\"streamz\":\"/mwVrzJ290lzATbpMg22Fk4MVBcs.png\",\"disney.com\":\"/7Lpp3EiQyEWnHWUlDIBtr2Jzp92.png\",\"espn+\":\"/mnrrbM7g7gvthN6iyYZ8ndrGKix.png\",\"court tv\":\"/uZHvgQG1ThBWbmDH6Os3YL6sA9J.png\",\"hbo go\":\"/3UnuWwVr6PCtXyGzZZqgYQTZhCq.png\",\"sky documentaries\":\"/ePgfopNKnZ6iSRMh1Gcm7IIamh1.png\",\"commercial television\":\"/d8K1SKholLjEPHaY5evDwRR7wgj.png\",\"noovo\":\"/5dvZFVBad4gj3vdCFY3kBHlIwg3.png\",\"山西电视台\":\"/7SfN4GyCU58ijp0DQJpKdGJURHV.png\",\"tgzg\":\"/zGbaSOBIXizdsGvxbWusDEGCGtd.png\",\"star bharat\":\"/xGtgKrr8vyXSJLjAteCBeCljvwM.png\",\"fuji news network\":\"/tjDkrfSswzDxx57bXnVwcx8QRUQ.png\",\"mbs動画イズム\":\"/esUBmXT8s6wZrzRYYb3AGmhNkuo.png\",\"rthk\":\"/szBBidFy2MHGUejAtu4EjaiS3Mt.png\",\"a+\":\"/r9ekzGSuDr1cIrJ4jenm6iPxwRk.png\",\"accuweather\":\"/accuweather.png\",\"amazon fire tv\":\"/amazon_fire_tv.png\",\"amazon prime\":\"/amazon_prime.png\",\"android tv\":\"/android_tv.png\",\"aol on\":\"/aol_on.png\",\"apple tv\":\"/apple_tv.png\",\"canal digitaal\":\"/canal_digitaal.png\",\"chromecast\":\"/chromecast.png\",\"filmbox live\":\"/filmbox_live.png\",\"funbox\":\"/funbox.png\",\"karaoke channel\":\"/karaoke_channel.png\",\"kpn\":\"/kpn.png\",\"national geographic kids\":\"/national_geographic_kids.png\",\"nintendo switch\":\"/nintendo_switch.png\",\"nintendo wii\":\"/nintendo_wii.png\",\"nl ziet\":\"/nl_ziet.png\",\"npo start\":\"/npo_start.png\",\"nvidia shield\":\"/nvidia_shield.png\",\"pandora\":\"/pandora.png\",\"pathe thuis\":\"/pathe_thuis.png\",\"playstation\":\"/playstation.png\",\"plex\":\"/plex.png\",\"plutotv\":\"/plutotv.png\",\"rtl xl\":\"/rtl_xl.png\",\"spotify\":\"/spotify.png\",\"steam link\":\"/steam_link.png\",\"stremio\":\"/stremio.png\",\"ted\":\"/ted.png\",\"thuisbezorgd\":\"/thuisbezorgd.png\",\"tubi\":\"/tubi.png\",\"ufc\":\"/ufc.png\",\"veoh\":\"/veoh.png\",\"viewster\":\"/viewster.png\",\"xbox\":\"/xbox.png\",\"youtube kids\":\"/youtube kids.png\",\"kuwait tv\":\"/hEV0q3GBV4pDIkLbQdlt4NRPwS1.png\",\"c2s network\":\"/kpZyuoVeHOhPvFrSmBu7iGJ5jGm.png\",\"astro maya\":\"/3xJVfJa4df96AuMitba1rGko5yP.png\",\"iwant\":\"/C9LDkqA2vosa6ibxue9gZrCaQr.png\",\"ais play\":\"/yeAj99gPJjTbTLsWxy4eXqJv0hU.png\",\"skai\":\"/sAJZ8o5kVDhetos2SsLTZL7n7ZP.png\",\"shanxi television\":\"/7SfN4GyCU58ijp0DQJpKdGJURHV.png\",\"tzgz\":\"/zGbaSOBIXizdsGvxbWusDEGCGtd.png\",\"radiodiffusion télévision française rtf\":\"/gqZtBobtvJupQAGPGL0HpErWPeG.png\",\"astro aec\":\"/7AvGS1q9vYjUs2GkVKnoypLC0nv.png\",\"f1 tv\":\"/eGPTbt05LYgBniGaj8NFusMzRWm.png\",\"tl streaming\":\"/bwjj3k5haH2cXpONzngRlt1QO3o.png\",\"ua: pershyi\":\"/iPUQ6RicbWvOQxqyr0vJNlz9HOr.png\",\"24 kanal\":\"/8D7vVsJzuNKzlKLvAvK4hK2eCDF.png\",\"hromadske\":\"/p5eGMTKV3jABp6yoQljnj33bvcj.png\",\"ctv comedy channel\":\"/l49FKtSEfGaPDPffzsSOn0wKRDL.png\",\"ziggo sport\":\"/1Bb3dz4RZG6ymUOfYcCBgGaaw3f.png\",\"rtl z\":\"/8594cSaaSi9GBTtzdVoixvZODhx.png\",\"rtl 7\":\"/cnkoWK84gsVqPYa4NBCFz74chhz.png\",\"rtl 8\":\"/AjHAm84qS6CmzPV0tGLGmwMgXiP.png\",\"sbs 9\":\"/2l7Sw99wIpAlBKyxXY1u2YWHVoH.png\",\"ziggo tv\":\"/j3ITOYByfBOqB8brTtGLdLetLNe.png\",\"duck tv\":\"/paWTgAPLyViNI5L6nAXgh4SEj3E.png\",\"channel awesome\":\"/xXprcFVtay4af4ScxEZRByQg793.png\",\"巴哈姆特動畫瘋 (tw)\":\"/786RBHyCRB8ymRI7F9YbdIGvsGs.png\"}"
  },
  {
    "path": "custom_components/samsungtv_smart/manifest.json",
    "content": "{\n  \"domain\": \"samsungtv_smart\",\n  \"name\": \"SamsungTV Smart\",\n  \"after_dependencies\": [\"smartthings\"],\n  \"codeowners\": [\"@jaruba\", \"@ollo69\", \"@screwdgeh\"],\n  \"config_flow\": true,\n  \"dependencies\": [\"http\"],\n  \"documentation\": \"https://github.com/ollo69/ha-samsungtv-smart\",\n  \"integration_type\": \"device\",\n  \"iot_class\": \"cloud_polling\",\n  \"issue_tracker\": \"https://github.com/ollo69/ha-samsungtv-smart/issues\",\n  \"requirements\": [\n    \"websocket-client!=1.4.0,>=0.58.0\",\n    \"wakeonlan>=2.0.0\",\n    \"aiofiles>=0.8.0\",\n    \"casttube>=0.2.1\"\n  ],\n  \"version\": \"0.14.5\"\n}\n"
  },
  {
    "path": "custom_components/samsungtv_smart/media_player.py",
    "content": "\"\"\"Support for interface with an Samsung TV.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc import Callable\nfrom datetime import datetime, timedelta, timezone\nfrom enum import Enum\nimport logging\nfrom socket import error as socketError\nfrom time import sleep\nfrom typing import Any\nfrom urllib.parse import parse_qs, urlparse\n\nfrom aiohttp import ClientConnectionError, ClientResponseError, ClientSession\nimport async_timeout\nimport voluptuous as vol\nfrom wakeonlan import send_magic_packet\nfrom websocket import WebSocketTimeoutException\n\nfrom homeassistant.components import media_source\nfrom homeassistant.components.media_player import (\n    ATTR_MEDIA_ENQUEUE,\n    MediaPlayerDeviceClass,\n    MediaPlayerEnqueue,\n    MediaPlayerEntity,\n    MediaPlayerEntityFeature,\n    MediaPlayerState,\n    MediaType,\n)\nfrom homeassistant.components.media_player.browse_media import (\n    async_process_play_media_url,\n)\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import (\n    CONF_API_KEY,\n    CONF_BROADCAST_ADDRESS,\n    CONF_DEVICE_ID,\n    CONF_HOST,\n    CONF_PORT,\n    CONF_SERVICE,\n    CONF_SERVICE_DATA,\n    CONF_TIMEOUT,\n    CONF_TOKEN,\n    SERVICE_TURN_OFF,\n    SERVICE_TURN_ON,\n    STATE_OFF,\n    STATE_ON,\n)\nfrom homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback\nfrom homeassistant.exceptions import HomeAssistantError\nfrom homeassistant.helpers import config_validation as cv, entity_platform\nfrom homeassistant.helpers.aiohttp_client import async_get_clientsession\nfrom homeassistant.helpers.dispatcher import async_dispatcher_connect\nfrom homeassistant.helpers.service import CONF_SERVICE_ENTITY_ID, async_call_from_config\nfrom homeassistant.helpers.storage import STORAGE_DIR\nfrom homeassistant.util import Throttle, dt as dt_util\nfrom homeassistant.util.async_ import run_callback_threadsafe\n\nfrom . import get_smartthings_api_key\nfrom .api.samsungcast import SamsungCastTube\nfrom .api.samsungws import ArtModeStatus, SamsungTVAsyncRest, SamsungTVWS\nfrom .api.smartthings import SmartThingsTV, STStatus\nfrom .api.upnp import SamsungUPnP\nfrom .const import (\n    CONF_APP_LAUNCH_METHOD,\n    CONF_APP_LIST,\n    CONF_APP_LOAD_METHOD,\n    CONF_CHANNEL_LIST,\n    CONF_DUMP_APPS,\n    CONF_EXT_POWER_ENTITY,\n    CONF_LOGO_OPTION,\n    CONF_PING_PORT,\n    CONF_POWER_ON_METHOD,\n    CONF_SHOW_CHANNEL_NR,\n    CONF_SOURCE_LIST,\n    CONF_ST_ENTRY_UNIQUE_ID,\n    CONF_SYNC_TURN_OFF,\n    CONF_SYNC_TURN_ON,\n    CONF_TOGGLE_ART_MODE,\n    CONF_USE_LOCAL_LOGO,\n    CONF_USE_MUTE_CHECK,\n    CONF_USE_ST_CHANNEL_INFO,\n    CONF_USE_ST_STATUS_INFO,\n    CONF_WOL_REPEAT,\n    CONF_WS_NAME,\n    DATA_CFG,\n    DATA_OPTIONS,\n    DEFAULT_APP,\n    DEFAULT_PORT,\n    DEFAULT_SOURCE_LIST,\n    DEFAULT_TIMEOUT,\n    DOMAIN,\n    LOCAL_LOGO_PATH,\n    MAX_WOL_REPEAT,\n    SERVICE_SELECT_PICTURE_MODE,\n    SERVICE_SET_ART_MODE,\n    SIGNAL_CONFIG_ENTITY,\n    STD_APP_LIST,\n    WS_PREFIX,\n    AppLaunchMethod,\n    AppLoadMethod,\n    PowerOnMethod,\n)\nfrom .entity import SamsungTVEntity\nfrom .logo import LOGO_OPTION_DEFAULT, LocalImageUrl, Logo, LogoOption\n\nATTR_ART_MODE_STATUS = \"art_mode_status\"\nATTR_IP_ADDRESS = \"ip_address\"\nATTR_PICTURE_MODE = \"picture_mode\"\nATTR_PICTURE_MODE_LIST = \"picture_mode_list\"\n\nCMD_OPEN_BROWSER = \"open_browser\"\nCMD_RUN_APP = \"run_app\"\nCMD_RUN_APP_REMOTE = \"run_app_remote\"\nCMD_RUN_APP_REST = \"run_app_rest\"\nCMD_SEND_KEY = \"send_key\"\nCMD_SEND_TEXT = \"send_text\"\n\nDELAYED_SOURCE_TIMEOUT = 80\nKEYHOLD_MAX_DELAY = 5.0\nKEYPRESS_DEFAULT_DELAY = 0.5\nKEYPRESS_MAX_DELAY = 2.0\nKEYPRESS_MIN_DELAY = 0.2\nMAX_ST_ERROR_COUNT = 4\nMEDIA_TYPE_BROWSER = \"browser\"\nMEDIA_TYPE_KEY = \"send_key\"\nMEDIA_TYPE_TEXT = \"send_text\"\nPOWER_OFF_DELAY = 20\nST_APP_SEPARATOR = \"/\"\nST_UPDATE_TIMEOUT = 5\n\nYT_APP_IDS = (\"111299001912\", \"9Ur5IzDKqV.TizenYouTube\")\nYT_VIDEO_QS = \"v\"\nYT_SVIDEO = \"/shorts/\"\n\nMAX_CONTROLLED_ENTITY = 4\n\nSUPPORT_SAMSUNGTV_SMART = (\n    MediaPlayerEntityFeature.PAUSE\n    | MediaPlayerEntityFeature.VOLUME_SET\n    | MediaPlayerEntityFeature.VOLUME_STEP\n    | MediaPlayerEntityFeature.VOLUME_MUTE\n    | MediaPlayerEntityFeature.PREVIOUS_TRACK\n    | MediaPlayerEntityFeature.NEXT_TRACK\n    | MediaPlayerEntityFeature.SELECT_SOURCE\n    | MediaPlayerEntityFeature.TURN_OFF\n    | MediaPlayerEntityFeature.TURN_ON\n    | MediaPlayerEntityFeature.PLAY\n    | MediaPlayerEntityFeature.PLAY_MEDIA\n    | MediaPlayerEntityFeature.STOP\n)\n\nMIN_TIME_BETWEEN_ST_UPDATE = timedelta(seconds=5)\nST_API_KEY_UPDATE_INTERVAL = timedelta(minutes=30)\nSCAN_INTERVAL = timedelta(seconds=15)\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant, entry: ConfigEntry, async_add_entities\n) -> None:\n    \"\"\"Set up the Samsung TV from a config entry.\"\"\"\n\n    # session used by aiohttp\n    session = async_get_clientsession(hass)\n    local_logo_path = hass.data[DOMAIN].get(LOCAL_LOGO_PATH)\n    config = hass.data[DOMAIN][entry.entry_id][DATA_CFG]\n\n    logo_file = hass.config.path(STORAGE_DIR, f\"{DOMAIN}_logo_paths\")\n\n    def update_token_func(token: str, token_key: str) -> None:\n        \"\"\"Update config entry with the new token.\"\"\"\n        hass.config_entries.async_update_entry(\n            entry, data={**entry.data, token_key: token}\n        )\n\n    async_add_entities(\n        [\n            SamsungTVDevice(\n                config,\n                entry.entry_id,\n                hass.data[DOMAIN][entry.entry_id],\n                session,\n                update_token_func,\n                logo_file,\n                local_logo_path,\n            )\n        ],\n        True,\n    )\n\n    # register services\n    platform = entity_platform.current_platform.get()\n    platform.async_register_entity_service(\n        SERVICE_SELECT_PICTURE_MODE,\n        {vol.Required(ATTR_PICTURE_MODE): cv.string},\n        \"async_select_picture_mode\",\n    )\n    platform.async_register_entity_service(\n        SERVICE_SET_ART_MODE,\n        {},\n        \"async_set_art_mode\",\n    )\n\n\ndef _get_default_app_info(app_id):\n    \"\"\"Get information for default app.\"\"\"\n    if not app_id:\n        return None, None, None\n\n    if app_id in STD_APP_LIST:\n        info = STD_APP_LIST[app_id]\n        return app_id, info.get(\"st_app_id\"), info.get(\"logo\")\n\n    for info in STD_APP_LIST.values():\n        st_app_id = info.get(\"st_app_id\", \"\")\n        if st_app_id == app_id:\n            return app_id, None, info.get(\"logo\")\n    return None, None, None\n\n\nclass ArtModeSupport(Enum):\n    \"\"\"Define ArtMode support lever.\"\"\"\n\n    UNSUPPORTED = 0\n    PARTIAL = 1\n    FULL = 2\n\n\nclass SamsungTVDevice(SamsungTVEntity, MediaPlayerEntity):\n    \"\"\"Representation of a Samsung TV.\"\"\"\n\n    _attr_device_class = MediaPlayerDeviceClass.TV\n    _attr_name = None\n\n    def __init__(\n        self,\n        config: dict[str, Any],\n        entry_id: str,\n        entry_data: dict[str, Any] | None,\n        session: ClientSession,\n        update_token_func: Callable[[str, str], None],\n        logo_file: str,\n        local_logo_path: str | None,\n    ) -> None:\n        \"\"\"Initialize the Samsung device.\"\"\"\n\n        super().__init__(config, entry_id)\n\n        self._entry_data = entry_data\n        self._host = config[CONF_HOST]\n\n        # Set entity attributes\n        self._attr_media_title = None\n        self._attr_media_image_url = None\n        self._attr_media_image_remotely_accessible = False\n\n        # Assume that the TV is not muted and volume is 0\n        self._attr_is_volume_muted = False\n        self._attr_volume_level = 0.0\n\n        # Device information from TV\n        self._device_info: dict[str, Any] | None = None\n\n        # Save a reference to the imported config\n        self._broadcast = config.get(CONF_BROADCAST_ADDRESS)\n\n        # Assume that the TV is in Play mode and state is off\n        self._playing = True\n        self._state = MediaPlayerState.OFF\n\n        # Mark the end of a shutdown command (need to wait 15 seconds before\n        # sending the next command to avoid turning the TV back ON).\n        self._started_up = False\n        self._end_of_power_off = None\n        self._fake_on = None\n        self._delayed_set_source = None\n        self._delayed_set_source_time = None\n\n        # generic for sources and apps\n        self._source = None\n        self._running_app = None\n        self._yt_app_id = None\n\n        # prepare TV lists options\n        self._default_source_used = False\n        self._source_list = None\n        self._dump_apps = True\n        self._app_list = None\n        self._app_list_st = None\n        self._channel_list = None\n\n        # config options reloaded on change\n        self._use_st_status: bool = True\n        self._use_channel_info: bool = True\n        self._use_mute_check: bool = False\n        self._show_channel_number: bool = False\n\n        # ws initialization\n        ws_name = config.get(CONF_WS_NAME, self._name)\n        self._ws = SamsungTVWS(\n            host=self._host,\n            token=config.get(CONF_TOKEN),\n            port=config.get(CONF_PORT, DEFAULT_PORT),\n            timeout=config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),\n            key_press_delay=KEYPRESS_DEFAULT_DELAY,\n            name=f\"{WS_PREFIX} {ws_name}\",  # this is the name shown in the TV external device.\n        )\n\n        def new_token_callback():\n            \"\"\"Update config entry with the new token.\"\"\"\n            run_callback_threadsafe(\n                self.hass.loop, update_token_func, self._ws.token, CONF_TOKEN\n            )\n\n        self._ws.register_new_token_callback(new_token_callback)\n\n        # rest api initialization\n        self._rest_api = SamsungTVAsyncRest(\n            host=self._host,\n            session=session,\n            timeout=DEFAULT_TIMEOUT,\n        )\n\n        # upnp initialization\n        self._upnp = SamsungUPnP(host=self._host, session=session)\n\n        # smartthings initialization\n        st_entry_uniqueid: str | None = config.get(CONF_ST_ENTRY_UNIQUE_ID)\n\n        def api_key_callback() -> str | None:\n            \"\"\"Get new api key and update config entry with the new token.\"\"\"\n            return self._update_smartthing_token(st_entry_uniqueid, update_token_func)\n\n        self._st = None\n        self._st_api_key = config.get(CONF_API_KEY)\n        device_id = config.get(CONF_DEVICE_ID)\n        if self._st_api_key and device_id:\n            use_callbck: bool = st_entry_uniqueid is not None\n            self._st = SmartThingsTV(\n                api_key=self._st_api_key,\n                device_id=device_id,\n                use_channel_info=True,\n                session=session,\n                api_key_callback=api_key_callback if use_callbck else None,\n            )\n\n        self._st_error_count = 0\n        self._st_last_exc = None\n        self._setvolumebyst = False\n\n        # logo control initializzation\n        self._local_image_url = LocalImageUrl(local_logo_path)\n        self._logo_option = LOGO_OPTION_DEFAULT\n        self._logo = Logo(\n            logo_option=self._logo_option,\n            logo_file_download=logo_file,\n            session=session,\n        )\n\n        # YouTube cast\n        self._cast_api = SamsungCastTube(self._host)\n\n        # update config options for first time\n        self._update_config_options(True)\n\n    @Throttle(ST_API_KEY_UPDATE_INTERVAL)\n    @callback\n    def _update_smartthing_token(\n        self, st_unique_id: str, update_token_func: Callable[[str, str], None]\n    ) -> str | None:\n        \"\"\"Update the smartthing token when change on native integration.\"\"\"\n        _LOGGER.debug(\"Trying to update smartthing access token\")\n        if not (new_token := get_smartthings_api_key(self.hass, st_unique_id)):\n            _LOGGER.warning(\n                \"Failed to retrieve SmartThings integration access token,\"\n                \" using last available\"\n            )\n            return self._st_api_key\n\n        if new_token != self._st_api_key:\n            _LOGGER.info(\"SmartThings access token updated\")\n            update_token_func(new_token, CONF_API_KEY)\n            self._st_api_key = new_token\n\n        return self._st_api_key\n\n    async def async_added_to_hass(self):\n        \"\"\"Set config parameter when add to hass.\"\"\"\n        await super().async_added_to_hass()\n\n        # this will update config options when changed\n        self.async_on_remove(\n            async_dispatcher_connect(\n                self.hass, SIGNAL_CONFIG_ENTITY, self._update_config_options\n            )\n        )\n\n        def update_status_callback():\n            \"\"\"Update current TV status.\"\"\"\n            run_callback_threadsafe(self.hass.loop, self._status_changed_callback)\n\n        self._ws.register_status_callback(update_status_callback)\n        await self.hass.async_add_executor_job(self._ws.start_poll)\n\n    async def async_will_remove_from_hass(self):\n        \"\"\"Run when entity will be removed from hass.\"\"\"\n        self._ws.unregister_status_callback()\n        await self.hass.async_add_executor_job(self._ws.stop_poll)\n\n    @staticmethod\n    def _split_app_list(app_list: dict[str, str]) -> list[dict[str, str]]:\n        \"\"\"Split the application list for standard and SmartThings.\"\"\"\n        apps = {}\n        apps_st = {}\n\n        for app_name, app_ids in app_list.items():\n            try:\n                app_id_split = app_ids.split(ST_APP_SEPARATOR, 1)\n            except (ValueError, AttributeError):\n                _LOGGER.warning(\n                    \"Invalid ID [%s] for App [%s] will be ignored.\"\n                    \" Use integration options to correct the App ID\",\n                    app_ids,\n                    app_name,\n                )\n                continue\n\n            app_id = app_id_split[0]\n            if len(app_id_split) == 1:\n                _, st_app_id, _ = _get_default_app_info(app_id)\n            else:\n                st_app_id = app_id_split[1]\n\n            apps[app_name] = app_id\n            apps_st[app_name] = st_app_id or app_id\n\n        return [apps, apps_st]\n\n    def _load_tv_lists(self, first_load=False):\n        \"\"\"Load TV sources, apps and channels.\"\"\"\n\n        # load sources list\n        default_source_used = False\n        source_list = self._get_option(CONF_SOURCE_LIST, {})\n        if not source_list:\n            source_list = DEFAULT_SOURCE_LIST\n            default_source_used = True\n        self._source_list = source_list\n        self._default_source_used = default_source_used\n\n        # load apps list\n        app_list = self._get_option(CONF_APP_LIST, {})\n        if app_list:\n            double_list = self._split_app_list(app_list)\n            self._app_list = double_list[0]\n            self._app_list_st = double_list[1]\n        else:\n            self._app_list = None if first_load else {}\n            self._app_list_st = None if first_load else {}\n\n        # load channels list\n        self._channel_list = self._get_option(CONF_CHANNEL_LIST, {})\n\n    @callback\n    def _update_config_options(self, first_load=False):\n        \"\"\"Update config options.\"\"\"\n        self._load_tv_lists(first_load)\n        self._use_st_status = self._get_option(CONF_USE_ST_STATUS_INFO, True)\n        self._use_channel_info = self._get_option(CONF_USE_ST_CHANNEL_INFO, True)\n        self._use_mute_check = self._get_option(CONF_USE_MUTE_CHECK, False)\n        self._show_channel_number = self._get_option(CONF_SHOW_CHANNEL_NR, False)\n        self._ws.update_app_list(self._app_list)\n        self._ws.set_ping_port(self._get_option(CONF_PING_PORT, 0))\n\n    @callback\n    def _status_changed_callback(self):\n        \"\"\"Called when status changed.\"\"\"\n        _LOGGER.debug(\"status_changed_callback called\")\n        self.async_schedule_update_ha_state(True)\n\n    def _get_option(self, param, default=None):\n        \"\"\"Get option from entity configuration.\"\"\"\n        if not self._entry_data:\n            return default\n        option = self._entry_data[DATA_OPTIONS].get(param)\n        return default if option is None else option\n\n    def _get_device_spec(self, key: str) -> Any | None:\n        \"\"\"Check if a flag exists in latest device info.\"\"\"\n        if not ((info := self._device_info) and (device := info.get(\"device\"))):\n            return None\n        return device.get(key)\n\n    def _power_off_in_progress(self):\n        \"\"\"Check if a power off request is in progress.\"\"\"\n        return (\n            self._end_of_power_off is not None\n            and self._end_of_power_off > dt_util.utcnow()\n        )\n\n    async def _update_volume_info(self):\n        \"\"\"Update the volume info.\"\"\"\n        if self._state == MediaPlayerState.ON:\n            # if self._st and self._setvolumebyst:\n            #     self._attr_volume_level = self._st.volume\n            #     self._attr_is_volume_muted = self._st.muted\n            #     return\n\n            if (volume := await self._upnp.async_get_volume()) is not None:\n                self._attr_volume_level = int(volume) / 100\n            else:\n                self._attr_volume_level = None\n            self._attr_is_volume_muted = await self._upnp.async_get_mute()\n\n    def _get_external_entity_status(self):\n        \"\"\"Get status from external binary sensor.\"\"\"\n        if not (ext_entity := self._get_option(CONF_EXT_POWER_ENTITY)):\n            return True\n        return not self.hass.states.is_state(ext_entity, STATE_OFF)\n\n    async def _check_status(self):\n        \"\"\"Check TV status with WS and others method to check power status.\"\"\"\n\n        if self._get_device_spec(\"PowerState\") is not None:\n            _LOGGER.debug(\"Checking if TV %s is on using device info\", self._host)\n            # Ensure we get an updated value\n            info = await self._async_load_device_info(force=True)\n            return info is not None and info[\"device\"][\"PowerState\"] == \"on\"\n\n        result = self._ws.is_connected\n        if result and self._st:\n            if (\n                self._st.state == STStatus.STATE_OFF\n                and self._st.prev_state != STStatus.STATE_OFF\n                and self._state == MediaPlayerState.ON\n                and self._use_st_status\n            ):\n                result = False\n\n        if result:\n            result = self._get_external_entity_status()\n\n        if result:\n            if self._ws.artmode_status in (ArtModeStatus.On, ArtModeStatus.Unavailable):\n                result = False\n\n        return result\n\n    @callback\n    def _get_running_app(self):\n        \"\"\"Retrieve name of running apps.\"\"\"\n\n        st_running_app = None\n        if self._app_list is not None:\n            for app, app_id in self._app_list.items():\n                if app_running := self._ws.is_app_running(app_id):\n                    self._running_app = app\n                    return\n                if app_running is False:\n                    continue\n                if self._st and self._st.channel_name != \"\":\n                    st_app_id = self._app_list_st.get(app, \"\")\n                    if st_app_id == self._st.channel_name:\n                        st_running_app = app\n\n        self._running_app = st_running_app or DEFAULT_APP\n\n    def _get_st_sources(self):\n        \"\"\"Get sources from SmartThings.\"\"\"\n        if self._state != MediaPlayerState.ON or not self._st:\n            _LOGGER.debug(\n                \"Samsung TV is OFF or SmartThings not configured, _get_st_sources not executed\"\n            )\n            return\n\n        st_source_list = {}\n        source_list = self._st.source_list\n        if source_list:\n\n            def get_next_name(index):\n                if index >= len(source_list):\n                    return \"\"\n                next_input = source_list[index]\n                if not (\n                    next_input.upper() in [\"DIGITALTV\", \"TV\"]\n                    or next_input.startswith(\"HDMI\")\n                ):\n                    return next_input\n                return \"\"\n\n            for i, _ in enumerate(source_list):\n                try:\n                    # SmartThings source list is an array that may contain the input\n                    # or the assigned name, if we found a name that is not an input\n                    # we use it as input name\n                    input_name = source_list[i]\n                    is_tv = input_name.upper() in [\"DIGITALTV\", \"TV\"]\n                    is_hdmi = input_name.startswith(\"HDMI\")\n                    if is_tv or is_hdmi:\n                        input_type = \"ST_TV\" if is_tv else \"ST_\" + input_name\n                        if input_type in st_source_list.values():\n                            continue\n\n                        name = self._st.get_source_name(input_name)\n                        if not name:\n                            name = get_next_name(i + 1)\n                        st_source_list[name or input_name] = input_type\n\n                except Exception:  # pylint: disable=broad-except\n                    pass\n\n        if len(st_source_list) > 0:\n            _LOGGER.info(\n                \"Samsung TV: loaded sources list from SmartThings: %s\",\n                str(st_source_list),\n            )\n            self._source_list = st_source_list\n            self._default_source_used = False\n\n    def _gen_installed_app_list(self):\n        \"\"\"Get apps installed on TV.\"\"\"\n\n        if self._dump_apps:\n            self._dump_apps = self._get_option(CONF_DUMP_APPS, False)\n\n        if not (self._app_list is None or self._dump_apps):\n            return\n\n        app_list = self._ws.installed_app\n        if not app_list:\n            return\n\n        app_load_method = AppLoadMethod(\n            self._get_option(CONF_APP_LOAD_METHOD, AppLoadMethod.All.value)\n        )\n\n        # app_list is a list of dict\n        filtered_app_list = {}\n        filtered_app_list_st = {}\n        dump_app_list = {}\n        for app in app_list.values():\n            try:\n                app_name = app.app_name\n                app_id = app.app_id\n                def_app_id, st_app_id, _ = _get_default_app_info(app_id)\n                # app_list is automatically created only with apps in hard coded short\n                # list (STD_APP_LIST). Other available apps are dumped in a file that\n                # can be used to create a custom list.\n                # This is to avoid unuseful long list that can impact performance\n                if app_load_method != AppLoadMethod.NotLoad:\n                    if def_app_id or app_load_method == AppLoadMethod.All:\n                        filtered_app_list[app_name] = app_id\n                        filtered_app_list_st[app_name] = st_app_id or app_id\n\n                dump_app_list[app_name] = (\n                    app_id + ST_APP_SEPARATOR + st_app_id if st_app_id else app_id\n                )\n\n            except Exception:  # pylint: disable=broad-except\n                pass\n\n        if self._app_list is None:\n            self._app_list = filtered_app_list\n            self._app_list_st = filtered_app_list_st\n\n        if self._dump_apps:\n            _LOGGER.info(\n                \"List of available apps for SamsungTV %s: %s\",\n                self._host,\n                dump_app_list,\n            )\n            self._dump_apps = False\n\n    def _get_source(self):\n        \"\"\"Return the current input source.\"\"\"\n        if self.state != MediaPlayerState.ON:\n            self._source = None\n            return self._source\n\n        use_st: bool = self._st is not None and self._st.state == STStatus.STATE_ON\n        if self._running_app != DEFAULT_APP or not use_st:\n            self._source = self._running_app\n            return self._source\n\n        if self._st.source in [\"digitalTv\", \"TV\"]:\n            cloud_key = \"ST_TV\"\n        else:\n            cloud_key = \"ST_\" + self._st.source\n\n        found_source = self._running_app\n        for attr, value in self._source_list.items():\n            if value == cloud_key:\n                found_source = attr\n                break\n\n        self._source = found_source\n        return self._source\n\n    async def _smartthings_keys(self, source_key: str):\n        \"\"\"Manage the SmartThings key commands.\"\"\"\n        if not self._st:\n            _LOGGER.error(\n                \"SmartThings not configured. Command not valid: %s\", source_key\n            )\n            return False\n        if self._st.state != STStatus.STATE_ON:\n            _LOGGER.warning(\n                \"SmartThings not available. Command not sent: %s\", source_key\n            )\n            return False\n\n        if source_key.startswith(\"ST_HDMI\"):\n            await self._st.async_select_source(source_key.replace(\"ST_\", \"\"))\n        elif source_key == \"ST_TV\":\n            await self._st.async_select_source(\"digitalTv\")\n        elif source_key.startswith(\"ST_VD:\"):\n            if cmd := source_key.replace(\"ST_VD:\", \"\"):\n                await self._st.async_select_vd_source(cmd)\n        elif source_key == \"ST_CHUP\":\n            await self._st.async_send_command(\"stepchannel\", \"up\")\n        elif source_key == \"ST_CHDOWN\":\n            await self._st.async_send_command(\"stepchannel\", \"down\")\n        elif source_key.startswith(\"ST_CH\"):\n            ch_num = source_key.replace(\"ST_CH\", \"\")\n            if ch_num.isdigit():\n                await self._st.async_send_command(\"selectchannel\", ch_num)\n        elif source_key in [\"ST_MUTE\", \"ST_UNMUTE\"]:\n            await self._st.async_send_command(\n                \"audiomute\", \"off\" if source_key == \"ST_UNMUTE\" else \"on\"\n            )\n        elif source_key == \"ST_VOLUP\":\n            await self._st.async_send_command(\"stepvolume\", \"up\")\n        elif source_key == \"ST_VOLDOWN\":\n            await self._st.async_send_command(\"stepvolume\", \"down\")\n        elif source_key.startswith(\"ST_VOL\"):\n            vol_lev = source_key.replace(\"ST_VOL\", \"\")\n            if vol_lev.isdigit():\n                await self._st.async_send_command(\"setvolume\", vol_lev)\n        else:\n            raise ValueError(f\"Unsupported SmartThings command: {source_key}\")\n\n        return True\n\n    def _log_st_error(self, st_error: bool):\n        \"\"\"Log start or end problem in ST communication\"\"\"\n        if self._st_error_count == 0 and not st_error:\n            return\n\n        if st_error:\n            if self._st_error_count == MAX_ST_ERROR_COUNT:\n                return\n\n            self._st_error_count += 1\n            if self._st_error_count == MAX_ST_ERROR_COUNT:\n                msg_chk = \"Check connection status with TV on the phone App\"\n                if self._st_last_exc is not None:\n                    _LOGGER.error(\n                        \"%s - Error refreshing from SmartThings. %s. Error: %s\",\n                        self.entity_id,\n                        msg_chk,\n                        self._st_last_exc,\n                    )\n                else:\n                    _LOGGER.warning(\n                        \"%s - SmartThings report TV is off but status detected is on. %s\",\n                        self.entity_id,\n                        msg_chk,\n                    )\n            return\n\n        if self._st_error_count >= MAX_ST_ERROR_COUNT:\n            _LOGGER.warning(\"%s - Connection to SmartThings restored\", self.entity_id)\n        self._st_error_count = 0\n\n    async def _async_load_device_info(\n        self, force: bool = False\n    ) -> dict[str, Any] | None:\n        \"\"\"Try to gather infos of this TV.\"\"\"\n        if self._device_info is not None and not force:\n            return self._device_info\n\n        try:\n            device_info: dict[str, Any] = await self._rest_api.async_rest_device_info()\n            _LOGGER.debug(\"Device info on %s is: %s\", self._host, device_info)\n            self._device_info = device_info\n        except Exception as ex:  # pylint: disable=broad-except\n            _LOGGER.debug(\"Error retrieving device info on %s: %s\", self._host, ex)\n            return None\n\n        return self._device_info\n\n    @Throttle(MIN_TIME_BETWEEN_ST_UPDATE)\n    async def _async_st_update(self, **kwargs) -> bool | None:\n        \"\"\"Update SmartThings state of device.\"\"\"\n        try:\n            async with async_timeout.timeout(ST_UPDATE_TIMEOUT):\n                await self._st.async_device_update(self._use_channel_info)\n        except (\n            asyncio.TimeoutError,\n            ClientConnectionError,\n            ClientResponseError,\n        ) as exc:\n            _LOGGER.debug(\"%s - SmartThings error: [%s]\", self.entity_id, exc)\n            self._st_last_exc = exc\n            return False\n\n        self._st_last_exc = None\n        return True\n\n    async def async_update(self):\n        \"\"\"Update state of device.\"\"\"\n\n        # Required to get source and media title\n        st_error: bool | None = None\n        if self._st:\n            if (st_update := await self._async_st_update()) is not None:\n                st_error = not st_update\n\n        result = await self._check_status()\n        if not self._started_up or not result:\n            use_mute_check = False\n            self._fake_on = None\n        else:\n            use_mute_check = self._use_mute_check\n\n        if use_mute_check and self._state == MediaPlayerState.OFF:\n            first_detect = self._fake_on is None\n            if first_detect or self._fake_on is True:\n                if (is_muted := await self._upnp.async_get_mute()) is None:\n                    self._fake_on = True\n                else:\n                    self._fake_on = is_muted\n                if self._fake_on:\n                    if first_detect:\n                        _LOGGER.debug(\n                            \"%s - Detected fake power on, status not updated\",\n                            self.entity_id,\n                        )\n                    result = False\n\n        if st_error is not None:\n            if result and not st_error:\n                st_error = self._st.state != STStatus.STATE_ON\n            self._log_st_error(st_error)\n\n        self._state = MediaPlayerState.ON if result else MediaPlayerState.OFF\n        self._started_up = True\n\n        # NB: We are checking properties, not attribute!\n        if self.state == MediaPlayerState.ON:\n            if self._delayed_set_source:\n                difference = (\n                    datetime.now(timezone.utc) - self._delayed_set_source_time\n                ).total_seconds()\n                if difference > DELAYED_SOURCE_TIMEOUT:\n                    self._delayed_set_source = None\n                else:\n                    await self._async_select_source_delayed(self._delayed_set_source)\n            await self._async_load_device_info()\n            await self._update_volume_info()\n            self._get_running_app()\n            await self._update_media()\n\n        if self._state == MediaPlayerState.OFF:\n            self._end_of_power_off = None\n\n    def send_command(\n        self,\n        payload,\n        command_type=CMD_SEND_KEY,\n        key_press_delay: float = 0,\n        press=False,\n    ):\n        \"\"\"Send a key to the tv and handles exceptions.\"\"\"\n        if key_press_delay < 0:\n            key_press_delay = None  # means \"default\" provided with constructor\n\n        ret_val = False\n        try:\n            if command_type == CMD_RUN_APP:\n                ret_val = self._ws.run_app(payload)\n            elif command_type == CMD_RUN_APP_REMOTE:\n                app_cmd = payload.split(\",\")\n                app_id = app_cmd[0]\n                action_type = \"\"\n                meta_tag = \"\"\n                if len(app_cmd) > 1:\n                    action_type = app_cmd[1].strip()\n                if len(app_cmd) > 2:\n                    meta_tag = app_cmd[2].strip()\n                ret_val = self._ws.run_app(\n                    app_id, action_type, meta_tag, use_remote=True\n                )\n            elif command_type == CMD_RUN_APP_REST:\n                result = self._ws.rest_app_run(payload)\n                _LOGGER.debug(\"Rest API result launching app %s: %s\", payload, result)\n                ret_val = True\n            elif command_type == CMD_OPEN_BROWSER:\n                ret_val = self._ws.open_browser(payload)\n            elif command_type == CMD_SEND_TEXT:\n                ret_val = self._ws.send_text(payload)\n            elif command_type == CMD_SEND_KEY:\n                hold_delay = 0\n                source_keys = payload.split(\",\")\n                key_code = source_keys[0]\n                if len(source_keys) > 1:\n\n                    def get_hold_time():\n                        hold_time = source_keys[1].replace(\" \", \"\")\n                        if not hold_time:\n                            return 0\n                        if not hold_time.isdigit():\n                            return 0\n                        hold_time = int(hold_time) / 1000\n                        return min(hold_time, KEYHOLD_MAX_DELAY)\n\n                    hold_delay = get_hold_time()\n\n                if hold_delay > 0:\n                    ret_val = self._ws.hold_key(key_code, hold_delay)\n                else:\n                    ret_val = self._ws.send_key(\n                        key_code, key_press_delay, \"Press\" if press else \"Click\"\n                    )\n            else:\n                _LOGGER.debug(\"Send command: invalid command type -> %s\", command_type)\n\n        except (ConnectionResetError, AttributeError, BrokenPipeError):\n            _LOGGER.debug(\n                \"Error in send_command() -> ConnectionResetError/AttributeError/BrokenPipeError\"\n            )\n\n        except WebSocketTimeoutException:\n            _LOGGER.debug(\n                \"Failed sending payload %s command_type %s\",\n                payload,\n                command_type,\n                exc_info=True,\n            )\n\n        except OSError:\n            _LOGGER.debug(\"Error in send_command() -> OSError\")\n\n        return ret_val\n\n    async def async_send_command(\n        self,\n        payload,\n        command_type=CMD_SEND_KEY,\n        key_press_delay: float = 0,\n        press=False,\n    ):\n        \"\"\"Send a key to the tv in async mode.\"\"\"\n        return await self.hass.async_add_executor_job(\n            self.send_command, payload, command_type, key_press_delay, press\n        )\n\n    async def _update_media(self):\n        \"\"\"Update media and logo status.\"\"\"\n        logo_option_changed = False\n        new_media_title = self._get_new_media_title()\n\n        if not new_media_title:\n            self._attr_media_title = None\n            self._attr_media_image_url = None\n            self._attr_media_image_remotely_accessible = False\n            return\n\n        new_logo_option = LogoOption(\n            self._get_option(CONF_LOGO_OPTION, self._logo_option.value)\n        )\n        if self._logo_option != new_logo_option:\n            self._logo_option = new_logo_option\n            self._logo.set_logo_color(new_logo_option)\n            logo_option_changed = True\n\n        if not logo_option_changed:\n            logo_option_changed = self._logo.check_requested()\n\n        if not logo_option_changed:\n            if self._attr_media_title and new_media_title == self._attr_media_title:\n                return\n\n        _LOGGER.debug(\n            \"New media title is: %s, old media title is: %s, running app is: %s\",\n            new_media_title,\n            self._attr_media_title or \"<none>\",\n            self._running_app,\n        )\n\n        remote_access = False\n        if (media_image_url := await self._local_media_image(new_media_title)) is None:\n            media_image_url = await self._logo.async_find_match(new_media_title)\n            remote_access = media_image_url is not None\n\n        self._attr_media_title = new_media_title\n        self._attr_media_image_url = media_image_url\n        self._attr_media_image_remotely_accessible = remote_access\n\n    def _get_new_media_title(self):\n        \"\"\"Get the current media title.\"\"\"\n        if self._state != MediaPlayerState.ON:\n            return None\n\n        if self._running_app == DEFAULT_APP:\n            if self._st and self._st.state != STStatus.STATE_OFF:\n                if self._st.source in [\"digitalTv\", \"TV\"]:\n                    if self._st.channel_name != \"\":\n                        if self._show_channel_number and self._st.channel != \"\":\n                            return self._st.channel_name + \" (\" + self._st.channel + \")\"\n                        return self._st.channel_name\n                    if self._st.channel != \"\":\n                        return self._st.channel\n                    return None\n\n                if (run_app := self._st.channel_name) != \"\":\n                    # the channel name holds the running app ID\n                    # regardless of the self._cloud_source value\n                    # if the app ID is in the configured apps but is not running_app,\n                    # means that this is not the real running app / media title\n                    st_apps = self._app_list_st or {}\n                    if run_app not in list(st_apps.values()):\n                        return self._st.channel_name\n\n        media_title = self._get_source()\n        if media_title and media_title != DEFAULT_APP:\n            return media_title\n        return None\n\n    async def _local_media_image(self, media_title):\n        \"\"\"Get local media image if available.\"\"\"\n        if not self._get_option(CONF_USE_LOCAL_LOGO, True):\n            return None\n        app_id = media_title\n        if self._running_app != DEFAULT_APP:\n            if run_app_id := self._app_list.get(self._running_app):\n                app_id = run_app_id\n\n        _, _, logo_file = _get_default_app_info(app_id)\n        return await self.hass.async_add_executor_job(\n            self._local_image_url.get_image_url, media_title, logo_file\n        )\n\n    @property\n    def supported_features(self) -> int:\n        \"\"\"Flag media player features that are supported.\"\"\"\n        features = SUPPORT_SAMSUNGTV_SMART\n        if self.state == MediaPlayerState.ON:\n            features |= MediaPlayerEntityFeature.BROWSE_MEDIA\n        if self._st:\n            features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE\n        return features\n\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return the optional state attributes.\"\"\"\n        data = {ATTR_IP_ADDRESS: self._host}\n        if self._ws.artmode_status != ArtModeStatus.Unsupported:\n            status_on = self._ws.artmode_status == ArtModeStatus.On\n            data.update({ATTR_ART_MODE_STATUS: STATE_ON if status_on else STATE_OFF})\n        if self._st:\n            picture_mode = self._st.picture_mode\n            picture_mode_list = self._st.picture_mode_list\n            if picture_mode:\n                data[ATTR_PICTURE_MODE] = picture_mode\n            if picture_mode_list:\n                data[ATTR_PICTURE_MODE_LIST] = picture_mode_list\n\n        return data\n\n    @property\n    def media_channel(self):\n        \"\"\"Channel currently playing.\"\"\"\n        if self._state == MediaPlayerState.ON:\n            if self._st:\n                if self._st.source in [\"digitalTv\", \"TV\"] and self._st.channel != \"\":\n                    return self._st.channel\n        return None\n\n    @property\n    def media_content_type(self):\n        \"\"\"Return the content type of current playing media.\"\"\"\n        if self._state == MediaPlayerState.ON:\n            if self._running_app == DEFAULT_APP:\n                if self.media_channel:\n                    return MediaType.CHANNEL\n                return MediaType.VIDEO\n            return MediaType.APP\n        return None\n\n    @property\n    def app_id(self):\n        \"\"\"ID of the current running app.\"\"\"\n        if self._state != MediaPlayerState.ON:\n            return None\n\n        if self._app_list_st and self._running_app != DEFAULT_APP:\n            if app := self._app_list_st.get(self._running_app):\n                return app\n\n        if self._st:\n            if not self._st.channel and self._st.channel_name:\n                return self._st.channel_name\n        return DEFAULT_APP\n\n    @property\n    def state(self):\n        \"\"\"Return the state of the device.\"\"\"\n\n        # Warning: we assume that after a sending a power off command, the command is successful\n        # so for 20 seconds (defined in POWER_OFF_DELAY) the state will be off regardless of the\n        # actual state. This is to have better feedback to the command in the UI, but the logic\n        # might cause other issues in the future\n        if self._power_off_in_progress():\n            return MediaPlayerState.OFF\n\n        return self._state\n\n    @property\n    def source_list(self):\n        \"\"\"List of available input sources.\"\"\"\n        # try to get source list from SmartThings if a custom source list is not defined\n        if self._st and self._default_source_used:\n            self._get_st_sources()\n\n        self._gen_installed_app_list()\n\n        source_list = []\n        source_list.extend(list(self._source_list))\n        if self._app_list:\n            source_list.extend(list(self._app_list))\n        if self._channel_list:\n            source_list.extend(list(self._channel_list))\n        return source_list\n\n    @property\n    def channel_list(self):\n        \"\"\"List of available channels.\"\"\"\n        if not self._channel_list:\n            return None\n        return list(self._channel_list)\n\n    @property\n    def source(self):\n        \"\"\"Return the current input source.\"\"\"\n        return self._get_source()\n\n    @property\n    def sound_mode(self):\n        \"\"\"Name of the current sound mode.\"\"\"\n        if self._st:\n            return self._st.sound_mode\n        return None\n\n    @property\n    def sound_mode_list(self):\n        \"\"\"List of available sound modes.\"\"\"\n        if self._st:\n            return self._st.sound_mode_list or None\n        return None\n\n    @property\n    def support_art_mode(self) -> ArtModeSupport:\n        \"\"\"Return if art mode is supported.\"\"\"\n        if self._ws.artmode_status != ArtModeStatus.Unsupported:\n            return ArtModeSupport.FULL\n        if self._get_device_spec(\"FrameTVSupport\") == \"true\":\n            return ArtModeSupport.PARTIAL\n        return ArtModeSupport.UNSUPPORTED\n\n    def _send_wol_packet(self, wol_repeat=None):\n        \"\"\"Send a WOL packet to turn on the TV.\"\"\"\n        if not self._mac:\n            _LOGGER.error(\"MAC address not configured, impossible send WOL packet\")\n            return False\n\n        if not wol_repeat:\n            wol_repeat = self._get_option(CONF_WOL_REPEAT, 1)\n        wol_repeat = max(1, min(wol_repeat, MAX_WOL_REPEAT))\n        ip_address = self._broadcast or \"255.255.255.255\"\n        send_success = False\n        for i in range(wol_repeat):\n            if i > 0:\n                sleep(0.25)\n            try:\n                send_magic_packet(self._mac, ip_address=ip_address)\n                send_success = True\n            except socketError as exc:\n                _LOGGER.warning(\n                    \"Failed tentative n.%s to send WOL packet: %s\",\n                    i,\n                    exc,\n                )\n            except (TypeError, ValueError) as exc:\n                _LOGGER.error(\"Error sending WOL packet: %s\", exc)\n                return False\n\n        return send_success\n\n    async def _async_power_on(self, set_art_mode=False):\n        \"\"\"Turn the media player on.\"\"\"\n        cmd_power_on = \"KEY_POWER\"\n        cmd_power_art = \"KEY_POWER\"\n        if set_art_mode:\n            if self._ws.artmode_status == ArtModeStatus.Off:\n                # art mode from on\n                await self.async_send_command(cmd_power_art)\n                self._state = MediaPlayerState.OFF\n                return True\n\n        if self._ws.artmode_status == ArtModeStatus.On:\n            if set_art_mode:\n                return False\n            # power on from art mode\n            await self.async_send_command(cmd_power_art)\n            return True\n\n        if self.state != MediaPlayerState.OFF:\n            return False\n\n        result = True\n        if not await self.async_send_command(cmd_power_on):\n            turn_on_method = PowerOnMethod(\n                self._get_option(CONF_POWER_ON_METHOD, PowerOnMethod.WOL.value)\n            )\n\n            if turn_on_method == PowerOnMethod.SmartThings and self._st:\n                await self._st.async_turn_on()\n            else:\n                result = await self.hass.async_add_executor_job(self._send_wol_packet)\n\n        if result:\n            self._state = MediaPlayerState.OFF\n            self._end_of_power_off = None\n            self._ws.set_power_on_request(set_art_mode)\n\n        return result\n\n    async def _async_turn_on(self, set_art_mode=False):\n        \"\"\"Turn the media player on.\"\"\"\n        self._delayed_set_source = None\n        if not await self._async_power_on(set_art_mode):\n            return False\n        if self._state != MediaPlayerState.OFF:\n            return True\n\n        await self._async_switch_entity(not set_art_mode)\n\n        return True\n\n    async def async_turn_on(self):\n        \"\"\"Turn the media player on.\"\"\"\n        await self._async_turn_on()\n\n    async def async_set_art_mode(self):\n        \"\"\"Turn the media player on setting in art mode.\"\"\"\n        if (\n            self._state == MediaPlayerState.ON\n            and self.support_art_mode == ArtModeSupport.PARTIAL\n        ):\n            await self.async_send_command(\"KEY_POWER\")\n        elif self.support_art_mode == ArtModeSupport.FULL:\n            await self._async_turn_on(True)\n\n    def _turn_off(self):\n        \"\"\"Turn off media player.\"\"\"\n        if self._power_off_in_progress():\n            return False\n\n        cmd_power_off = \"KEY_POWER\"\n        cmd_power_art = \"KEY_POWER\"\n        self._ws.set_power_off_request()\n        if self._state == MediaPlayerState.ON:\n            if self.support_art_mode == ArtModeSupport.UNSUPPORTED:\n                self.send_command(cmd_power_off)\n            else:\n                self.send_command(f\"{cmd_power_art},3000\")\n        elif self._ws.artmode_status == ArtModeStatus.On:\n            self.send_command(f\"{cmd_power_art},3000\")\n        else:\n            return False\n\n        self._end_of_power_off = dt_util.utcnow() + timedelta(seconds=POWER_OFF_DELAY)\n\n        return True\n\n    async def async_turn_off(self):\n        \"\"\"Turn the media player on.\"\"\"\n        result = await self.hass.async_add_executor_job(self._turn_off)\n        if result:\n            await self._async_switch_entity(False)\n\n    async def async_toggle(self):\n        \"\"\"Toggle the power on the media player.\"\"\"\n        if (\n            self.state == MediaPlayerState.ON\n            and self.support_art_mode != ArtModeSupport.UNSUPPORTED\n        ):\n            if self._get_option(CONF_TOGGLE_ART_MODE, False):\n                await self.async_set_art_mode()\n                return\n        await super().async_toggle()\n\n    async def async_volume_up(self):\n        \"\"\"Volume up the media player.\"\"\"\n        if self._state != MediaPlayerState.ON:\n            return\n        await self.async_send_command(\"KEY_VOLUP\")\n        if self.volume_level is not None:\n            self._attr_volume_level = min(1.0, self.volume_level + 0.01)\n\n    async def async_volume_down(self):\n        \"\"\"Volume down media player.\"\"\"\n        if self._state != MediaPlayerState.ON:\n            return\n        await self.async_send_command(\"KEY_VOLDOWN\")\n        if self.volume_level is not None:\n            self._attr_volume_level = max(0.0, self.volume_level - 0.01)\n\n    async def async_mute_volume(self, mute):\n        \"\"\"Send mute command.\"\"\"\n        if self._state != MediaPlayerState.ON:\n            return\n        if self.is_volume_muted is not None and mute == self.is_volume_muted:\n            return\n        await self.async_send_command(\"KEY_MUTE\")\n        if self.is_volume_muted is not None:\n            self._attr_is_volume_muted = mute\n\n    async def async_set_volume_level(self, volume):\n        \"\"\"Set the volume level.\"\"\"\n        if self._state != MediaPlayerState.ON:\n            return\n        if self.volume_level is None:\n            return\n        if self._st and self._setvolumebyst:\n            await self._st.async_send_command(\"setvolume\", int(volume * 100))\n        else:\n            await self._upnp.async_set_volume(int(volume * 100))\n        self._attr_volume_level = volume\n\n    def media_play_pause(self):\n        \"\"\"Simulate play pause media player.\"\"\"\n        if self._playing:\n            self.media_pause()\n        else:\n            self.media_play()\n\n    def media_play(self):\n        \"\"\"Send play command.\"\"\"\n        self._playing = True\n        self.send_command(\"KEY_PLAY\")\n\n    def media_pause(self):\n        \"\"\"Send media pause command to media player.\"\"\"\n        self._playing = False\n        self.send_command(\"KEY_PAUSE\")\n\n    def media_stop(self):\n        \"\"\"Send media pause command to media player.\"\"\"\n        self._playing = False\n        self.send_command(\"KEY_STOP\")\n\n    def media_next_track(self):\n        \"\"\"Send next track command.\"\"\"\n        if self.media_channel:\n            self.send_command(\"KEY_CHUP\")\n        else:\n            self.send_command(\"KEY_FF\")\n\n    def media_previous_track(self):\n        \"\"\"Send the previous track command.\"\"\"\n        if self.media_channel:\n            self.send_command(\"KEY_CHDOWN\")\n        else:\n            self.send_command(\"KEY_REWIND\")\n\n    async def _async_send_keys(self, source_key):\n        \"\"\"Send key / chained keys.\"\"\"\n        prev_wait = True\n\n        if \"+\" in source_key:\n            all_source_keys = source_key.split(\"+\")\n            for this_key in all_source_keys:\n                if this_key.isdigit():\n                    prev_wait = True\n                    await asyncio.sleep(\n                        min(\n                            max((int(this_key) / 1000), KEYPRESS_MIN_DELAY),\n                            KEYPRESS_MAX_DELAY,\n                        )\n                    )\n                else:\n                    # put a default delay between key if set explicit\n                    if not prev_wait:\n                        await asyncio.sleep(KEYPRESS_DEFAULT_DELAY)\n                    prev_wait = False\n                    if this_key.startswith(\"ST_\"):\n                        await self._smartthings_keys(this_key)\n                    else:\n                        await self.async_send_command(this_key)\n\n            return True\n\n        if source_key.startswith(\"ST_\"):\n            return await self._smartthings_keys(source_key)\n\n        return await self.async_send_command(source_key)\n\n    async def _async_set_channel_source(self, channel_source=None):\n        \"\"\"Select the source for a channel.\"\"\"\n\n        if not channel_source:\n            if self._running_app == DEFAULT_APP:\n                return True\n            _LOGGER.error(\"Current source invalid for channel\")\n            return False\n\n        if self._source == channel_source:\n            return True\n\n        if channel_source not in self._source_list:\n            _LOGGER.error(\"Invalid channel source: %s\", channel_source)\n            return False\n\n        await self.async_select_source(channel_source)\n        if self._source != channel_source:\n            _LOGGER.error(\"Error selecting channel source: %s\", channel_source)\n            return False\n        await asyncio.sleep(3)\n\n        return True\n\n    async def _async_set_channel(self, channel):\n        \"\"\"Set a specific channel.\"\"\"\n\n        if channel.startswith(\"http\"):\n            await self.async_play_media(MediaType.URL, channel)\n            return True\n\n        channel_cmd = channel.split(\"@\")\n        channel_no = channel_cmd[0]\n        channel_source = None\n        if len(channel_cmd) > 1:\n            channel_source = channel_cmd[1]\n\n        try:\n            cv.positive_int(channel_no)\n        except vol.Invalid:\n            _LOGGER.error(\"Channel must be positive integer\")\n            return False\n\n        if not await self._async_set_channel_source(channel_source):\n            return False\n\n        if self._st:\n            return await self._smartthings_keys(f\"ST_CH{channel_no}\")\n\n        def send_digit():\n            for digit in channel_no:\n                self.send_command(\"KEY_\" + digit)\n                sleep(KEYPRESS_DEFAULT_DELAY)\n            self.send_command(\"KEY_ENTER\")\n\n        await self.hass.async_add_executor_job(send_digit)\n        return True\n\n    async def _async_launch_app(self, app_data, meta_data=None):\n        \"\"\"Launch app with different methods.\"\"\"\n\n        method = \"\"\n        app_cmd = app_data.split(\"@\")\n        app_id = app_cmd[0]\n        if self._app_list:\n            if app_id_from_list := self._app_list.get(app_id):\n                app_id = app_id_from_list\n        if meta_data:\n            app_id += f\",,{meta_data}\"\n            method = CMD_RUN_APP_REMOTE\n        elif len(app_cmd) > 1:\n            req_method = app_cmd[1].strip()\n            if req_method in (CMD_RUN_APP, CMD_RUN_APP_REMOTE, CMD_RUN_APP_REST):\n                method = req_method\n\n        if not method:\n            app_launch_method = AppLaunchMethod(\n                self._get_option(CONF_APP_LAUNCH_METHOD, AppLaunchMethod.Standard.value)\n            )\n\n            if app_launch_method == AppLaunchMethod.Remote:\n                method = CMD_RUN_APP_REMOTE\n            elif app_launch_method == AppLaunchMethod.Rest:\n                method = CMD_RUN_APP_REST\n            else:\n                method = CMD_RUN_APP\n\n        await self.async_send_command(app_id, method)\n\n    def _get_youtube_app_id(self):\n        \"\"\"Search youtube app id used to launch video.\"\"\"\n        if self._yt_app_id is not None:\n            return len(self._yt_app_id) > 0\n        if not self._app_list:\n            return False\n        self._yt_app_id = \"\"\n        for app_name, app_id in self._app_list.items():\n            if app_name.casefold().find(\"youtube\") >= 0:\n                if not self._yt_app_id:\n                    self._yt_app_id = app_id\n            if app_id in YT_APP_IDS:\n                self._yt_app_id = app_id\n                break\n\n        _LOGGER.debug(\"YouTube App ID: %s\", self._yt_app_id or \"not found\")\n        return len(self._yt_app_id) > 0\n\n    def _get_youtube_video_id(self, url):\n        \"\"\"Try to get youtube video id from url.\"\"\"\n        url_parsed = urlparse(url)\n        url_host = str(url_parsed.hostname).casefold()\n        url_path = url_parsed.path\n        if url_host.find(\"youtube\") < 0:\n            _LOGGER.debug(\"URL not related to Youtube\")\n            return None\n\n        video_id = None\n        url_query = parse_qs(url_parsed.query)\n        if YT_VIDEO_QS in url_query:\n            video_id = url_query[YT_VIDEO_QS][0]\n        elif url_path and str(url_path).casefold().startswith(YT_SVIDEO):\n            video_id = url_path[len(YT_SVIDEO) :]\n\n        if not video_id:\n            _LOGGER.warning(\"Youtube video ID not found in url: %s\", url)\n            return None\n\n        if not self._get_youtube_app_id():\n            _LOGGER.warning(\"Youtube app ID not available, configure in apps list\")\n            return None\n\n        _LOGGER.debug(\"Youtube video ID: %s\", video_id)\n        return video_id\n\n    def _cast_youtube_video(self, video_id: str, enqueue: MediaPlayerEnqueue):\n        \"\"\"\n        Cast a youtube video using samsungcast library.\n        This method is sync and must run in job executor.\n        \"\"\"\n        if enqueue == MediaPlayerEnqueue.PLAY:\n            self._cast_api.play_video(video_id)\n        elif enqueue == MediaPlayerEnqueue.NEXT:\n            self._cast_api.play_next(video_id)\n        elif enqueue == MediaPlayerEnqueue.ADD:\n            self._cast_api.add_to_queue(video_id)\n        elif enqueue == MediaPlayerEnqueue.REPLACE:\n            self._cast_api.clear_queue()\n            self._cast_api.play_video(video_id)\n\n    async def _async_play_youtube_video(\n        self, video_id: str, enqueue: MediaPlayerEnqueue\n    ):\n        \"\"\"Play a YouTube video using YouTube app.\"\"\"\n        run_app_id = None\n        if self._running_app != DEFAULT_APP:\n            run_app_id = self._app_list.get(self._running_app)\n\n        # launch youtube app if not running\n        if run_app_id != self._yt_app_id:\n            await self._async_launch_app(self._yt_app_id)\n            await asyncio.sleep(3)  # we wait for YouTube app to start\n\n        await self.hass.async_add_executor_job(\n            self._cast_youtube_video, video_id, enqueue\n        )\n\n    async def async_play_media(\n        self, media_type: MediaType | str, media_id: str, **kwargs\n    ):\n        \"\"\"Support running different media type command.\"\"\"\n        enqueue: MediaPlayerEnqueue | None = kwargs.get(ATTR_MEDIA_ENQUEUE)\n\n        if media_source.is_media_source_id(media_id):\n            media_type = MediaType.URL\n            play_item = await media_source.async_resolve_media(self.hass, media_id)\n            media_id = play_item.url\n        else:\n            media_type = media_type.lower()\n\n        if media_type in [MEDIA_TYPE_BROWSER, MediaType.URL]:\n            media_id = async_process_play_media_url(self.hass, media_id)\n            try:\n                cv.url(media_id)\n            except vol.Invalid:\n                _LOGGER.error('Media ID must be a valid url (ex: \"http://\"')\n                return\n\n        # Type channel\n        if media_type == MediaType.CHANNEL:\n            await self._async_set_channel(media_id)\n\n        # Launch an app\n        elif media_type == MediaType.APP:\n            await self._async_launch_app(media_id)\n\n        # Send custom key\n        elif media_type == MEDIA_TYPE_KEY:\n            try:\n                cv.string(media_id)\n            except vol.Invalid:\n                _LOGGER.error('Media ID must be a string (ex: \"KEY_HOME\"')\n                return\n\n            await self._async_send_keys(media_id)\n\n        # Open url or youtube app\n        elif media_type == MediaType.URL:\n            if enqueue and (video_id := self._get_youtube_video_id(media_id)):\n                await self._async_play_youtube_video(video_id, enqueue)\n                return\n\n            if await self._upnp.async_set_current_media(media_id):\n                self._playing = True\n                return\n\n            await self.async_send_command(media_id, CMD_OPEN_BROWSER)\n\n        # Open url in browser\n        elif media_type == MEDIA_TYPE_BROWSER:\n            await self.async_send_command(media_id, CMD_OPEN_BROWSER)\n\n        # Trying to make stream component work on TV\n        elif media_type == \"application/vnd.apple.mpegurl\":\n            if await self._upnp.async_set_current_media(media_id):\n                self._playing = True\n\n        elif media_type == MEDIA_TYPE_TEXT:\n            await self.async_send_command(media_id, CMD_SEND_TEXT)\n\n        else:\n            raise NotImplementedError(f\"Unsupported media type: {media_type}\")\n\n    async def async_browse_media(self, media_content_type=None, media_content_id=None):\n        \"\"\"Implement the websocket media browsing helper.\"\"\"\n        return await media_source.async_browse_media(self.hass, media_content_id)\n\n    async def async_select_source(self, source):\n        \"\"\"Select input source.\"\"\"\n        running_app = DEFAULT_APP\n        self._delayed_set_source = None\n\n        if self.state != MediaPlayerState.ON:\n            if await self._async_turn_on():\n                self._delayed_set_source = source\n                self._delayed_set_source_time = datetime.now(timezone.utc)\n            return\n\n        if self._source_list and source in self._source_list:\n            source_key = self._source_list[source]\n            if not await self._async_send_keys(source_key):\n                return\n        elif self._app_list and source in self._app_list:\n            app_id = self._app_list[source]\n            running_app = source\n            await self._async_launch_app(app_id)\n            if self._st:\n                self._st.set_application(self._app_list_st[source])\n        elif self._channel_list and source in self._channel_list:\n            source_key = self._channel_list[source]\n            await self._async_set_channel(source_key)\n            return\n        else:\n            _LOGGER.error(\"Unsupported source\")\n            return\n\n        self._running_app = running_app\n        self._source = source\n\n    async def _async_select_source_delayed(self, source):\n        \"\"\"Select input source with delayed ST option.\"\"\"\n        if self._st:\n            if self._st.state != STStatus.STATE_ON:\n                # wait for smartthings available\n                return\n\n        await self.async_select_source(source)\n\n    async def async_select_sound_mode(self, sound_mode):\n        \"\"\"Select sound mode.\"\"\"\n        if not self._st:\n            raise NotImplementedError()\n        await self._st.async_set_sound_mode(sound_mode)\n\n    async def async_select_picture_mode(self, picture_mode):\n        \"\"\"Select picture mode.\"\"\"\n        if not self._st:\n            raise NotImplementedError()\n        await self._st.async_set_picture_mode(picture_mode)\n\n    async def _async_switch_entity(self, power_on: bool):\n        \"\"\"Switch on/off related configure HA entity.\"\"\"\n\n        if power_on:\n            service_name = f\"{HA_DOMAIN}.{SERVICE_TURN_ON}\"\n            conf_entity = CONF_SYNC_TURN_ON\n        else:\n            service_name = f\"{HA_DOMAIN}.{SERVICE_TURN_OFF}\"\n            conf_entity = CONF_SYNC_TURN_OFF\n\n        entity_list = self._get_option(conf_entity)\n        if not entity_list:\n            return\n\n        for index, entity in enumerate(entity_list):\n            if index >= MAX_CONTROLLED_ENTITY:\n                _LOGGER.warning(\n                    \"SamsungTV Smart - Maximum %s entities can be controlled\",\n                    MAX_CONTROLLED_ENTITY,\n                )\n                break\n            if entity:\n                await _async_call_service(self.hass, service_name, entity)\n\n        return\n\n\nasync def _async_call_service(\n    hass,\n    service_name,\n    entity_id,\n    variable_data=None,\n):\n    \"\"\"Call a HA service.\"\"\"\n    service_data = {\n        CONF_SERVICE: service_name,\n        CONF_SERVICE_ENTITY_ID: entity_id,\n    }\n\n    if variable_data:\n        service_data[CONF_SERVICE_DATA] = variable_data\n\n    try:\n        await async_call_from_config(\n            hass,\n            service_data,\n            blocking=False,\n            validate_config=True,\n        )\n    except HomeAssistantError as ex:\n        _LOGGER.error(\"SamsungTV Smart - error %s\", ex)\n\n    return\n"
  },
  {
    "path": "custom_components/samsungtv_smart/remote.py",
    "content": "\"\"\"Support for the SamsungTV remote.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom datetime import datetime\nimport logging\nfrom typing import Any\n\nfrom homeassistant.components.media_player.const import (\n    ATTR_MEDIA_CONTENT_ID,\n    ATTR_MEDIA_CONTENT_TYPE,\n    DOMAIN as MP_DOMAIN,\n    SERVICE_PLAY_MEDIA,\n)\nfrom homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import (\n    CONF_SERVICE,\n    CONF_SERVICE_DATA,\n    SERVICE_TURN_OFF,\n    SERVICE_TURN_ON,\n)\nfrom homeassistant.core import HomeAssistant, callback\nfrom homeassistant.exceptions import HomeAssistantError\nfrom homeassistant.helpers import entity_registry as er\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.helpers.event import async_call_later\nfrom homeassistant.helpers.service import CONF_SERVICE_ENTITY_ID, async_call_from_config\n\nfrom .const import DATA_CFG, DOMAIN\nfrom .entity import SamsungTVEntity\nfrom .media_player import MEDIA_TYPE_KEY\n\nJOIN_COMMAND = \"+\"\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback\n) -> None:\n    \"\"\"Set up the Samsung TV from a config entry.\"\"\"\n\n    @callback\n    def _add_remote_entity(utc_now: datetime) -> None:\n        \"\"\"Create remote entity.\"\"\"\n        mp_entity_id = None\n        entity_reg = er.async_get(hass)\n        tv_entries = er.async_entries_for_config_entry(entity_reg, entry.entry_id)\n        for tv_entity in tv_entries:\n            if tv_entity.domain == MP_DOMAIN:\n                mp_entity_id = tv_entity.entity_id\n                break\n\n        if mp_entity_id is None:\n            return\n\n        config = hass.data[DOMAIN][entry.entry_id][DATA_CFG]\n        async_add_entities([SamsungTVRemote(config, entry.entry_id, mp_entity_id)])\n\n    # we wait for TV media player entity to be created\n    async_call_later(hass, 5, _add_remote_entity)\n\n\nclass SamsungTVRemote(SamsungTVEntity, RemoteEntity):\n    \"\"\"Device that sends commands to a SamsungTV.\"\"\"\n\n    _attr_name = None\n    _attr_should_poll = False\n\n    def __init__(self, config: dict[str, Any], entry_id: str, mp_entity_id: str):\n        \"\"\"Initialize the remote.\"\"\"\n        super().__init__(config, entry_id)\n        self._mp_entity_id = mp_entity_id\n\n    async def _async_call_service(\n        self,\n        service_name,\n        variable_data=None,\n    ):\n        \"\"\"Call a HA service.\"\"\"\n        service_data = {\n            CONF_SERVICE: f\"{MP_DOMAIN}.{service_name}\",\n            CONF_SERVICE_ENTITY_ID: self._mp_entity_id,\n        }\n\n        if variable_data:\n            service_data[CONF_SERVICE_DATA] = variable_data\n\n        try:\n            await async_call_from_config(\n                self.hass,\n                service_data,\n                blocking=True,\n                validate_config=True,\n            )\n        except HomeAssistantError as ex:\n            _LOGGER.error(\"SamsungTV Smart Remote - error %s\", ex)\n\n        return\n\n    async def async_turn_off(self, **kwargs: Any) -> None:\n        \"\"\"Turn the device off.\"\"\"\n\n        await self._async_call_service(SERVICE_TURN_OFF)\n\n    async def async_turn_on(self, **kwargs: Any) -> None:\n        \"\"\"Turn the device on.\"\"\"\n\n        await self._async_call_service(SERVICE_TURN_ON)\n\n    async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:\n        \"\"\"Send a command to a device.\n\n        Supported keys vary between models.\n        See https://github.com/jaruba/ha-samsungtv-tizen/blob/master/Key_codes.md\n        \"\"\"\n        num_repeats = kwargs[ATTR_NUM_REPEATS]\n        commands = JOIN_COMMAND.join(command)\n        content_id = commands\n        for _ in range(num_repeats - 1):\n            content_id += f\"{JOIN_COMMAND}{commands}\"\n\n        service_data = {\n            ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_KEY,\n            ATTR_MEDIA_CONTENT_ID: content_id,\n        }\n        await self._async_call_service(SERVICE_PLAY_MEDIA, service_data)\n"
  },
  {
    "path": "custom_components/samsungtv_smart/services.yaml",
    "content": "select_picture_mode:\n  name: Select Picture Mode\n  description: Send to samsung TV the command to change picture mode.\n  fields:\n    entity_id:\n      name: Entity Name\n      description: Name of the target entity\n      required: true\n      example: \"media_player.tv\"\n      selector:\n        entity:\n          integration: samsungtv_smart\n    picture_mode:\n      name: Picture Mode\n      description: Name of the picture mode to switch to. Possible options\n        can be found in the picture_mode_list state attribute.\n      required: true\n      example: \"Standard\"\n      selector:\n        text:\n\nset_art_mode:\n  name: Set Art Mode\n  description: Send to samsung TV the command to set art mode.\n  fields:\n    entity_id:\n      name: Entity Name\n      description: Name of the target entity\n      required: true\n      example: \"media_player.tv\"\n      selector:\n        entity:\n          integration: samsungtv_smart\n"
  },
  {
    "path": "custom_components/samsungtv_smart/translations/en.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"This Samsung TV is already configured.\",\n      \"already_in_progress\": \"Samsung TV configuration is already in progress.\",\n      \"host_unique_id\": \"Reconfiguration not possible beacause hostname is used as unique id. Please remove this entry and configure a new one.\",\n      \"reconfigure_successful\": \"Samsung TV reconfiguration completed successfully.\",\n      \"unsupported_version\": \"This integration require at least HomeAssistant version {req_ver}, you are running version {run_ver}.\"\n    },\n    \"error\": {\n      \"auth_missing\": \"Home Assistant is not authorized to connect to this Samsung TV. Please check your TV's settings to authorize Home Assistant.\",\n      \"invalid_host\": \"Invalid host.\",\n      \"not_successful\": \"Unable to create WebSocket connection with this Samsung TV device.\",\n      \"not_supported\": \"This Samsung TV device is currently not supported.\",\n      \"only_key_or_st\": \"Specify only access token or SmartThings entry.\",\n      \"st_api_key_fail\": \"Failed to retrieve access token from SmartThings entry.\",\n      \"st_device_not_found\": \"SmartThings TV deviceID not found.\",\n      \"st_device_used\": \"SmartThings TV deviceID already used.\",\n      \"wrong_api_key\": \"Wrong SmartThings token.\"\n    },\n    \"flow_title\": \"SamsungTV Smart: {model}\",\n    \"step\": {\n      \"confirm\": {\n        \"description\": \"Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization. Manual configurations for this TV will be overwritten.\"\n      },\n      \"user\": {\n        \"data\": {\n          \"host\": \"Host or IP address\",\n          \"name\": \"Name assigned to the entity\",\n          \"use_ha_name_for_ws\": \"Use HA instance name for identification on TV\",\n          \"api_key\": \"SmartThings generated token (optional)\",\n          \"st_entry_unique_id\": \"SmartThings entry used to provide SmartThings credential\"\n        },\n        \"description\": \"Enter your Samsung TV information. SmartThings is optional but really suggested.\\nAfter confirm you should see a popup on your TV asking for authorization.\"\n      },\n      \"reconfigure\": {\n        \"data\": {\n          \"host\": \"Host or IP address\",\n          \"api_key\": \"SmartThings generated token (optional, if empty will be used the existing)\",\n          \"st_entry_unique_id\": \"SmartThings entry used to provide SmartThings credential\"\n        },\n        \"description\": \"Enter your Samsung TV information to update. SmartThings configuration can be changed but not removed.\\nAfter confirm you could see a popup on your TV asking for authorization.\"\n      },\n      \"stdevice\": {\n        \"data\": {\n          \"st_devices\": \"SmartThings TV\"\n        },\n        \"description\": \"You have multiple TVs configured on your account. Select the TV you are configuring from the list.\"\n      },\n      \"stdeviceid\": {\n        \"data\": {\n          \"device_id\": \"SmartThings TV deviceID\"\n        },\n        \"description\": \"Automatic SmartThings deviceID detection failed. To continue you must identify the deviceID on the SmartThings site (see documentation) and insert it here.\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"title\": \"SamsungTV Smart options\",\n        \"data\": {\n          \"use_st_status_info\": \"Use SmartThings TV Status information\",\n          \"use_st_channel_info\": \"Use SmartThings TV Channels information\",\n          \"show_channel_number\": \"Use SmartThings TV Channels number information\",\n          \"app_load_method\": \"Applications list load mode at startup\",\n          \"logo_option\": \"Display a logo for known sources, apps and channels\",\n          \"use_local_logo\": \"Allow use of local logo images\",\n          \"power_on_method\": \"Method used to turn on TV\",\n          \"show_adv_opt\": \"Show options menu\"\n        }\n      },\n      \"menu\": {\n        \"title\": \"SamsungTV Smart options menu\",\n        \"menu_options\": {\n          \"adv_opt\": \"Advanced options\",\n          \"app_list\": \"Applications list configuration\",\n          \"channel_list\": \"Channels list configuration\",\n          \"init\": \"Standard options\",\n          \"save_exit\": \"Save options and exit\",\n          \"source_list\": \"Sources list configuration\",\n          \"sync_ent\": \"Synched entities configuration\"\n        }\n      },\n      \"adv_opt\": {\n        \"title\": \"SamsungTV Smart advanced options\",\n        \"data\": {\n          \"app_launch_method\": \"Applications launch method used\",\n          \"dump_apps\": \"Dump apps list on log file at startup (when possible)\",\n          \"use_mute_check\": \"Use volume mute status to detect fake power ON\",\n          \"wol_repeat\": \"Number of time WOL packet is sent to turn on TV\",\n          \"power_on_delay\": \"Seconds to delay power ON status\",\n          \"ping_port\": \"TCP port used to check power status (0 for ICMP)\",\n          \"ext_power_entity\": \"Binary sensor to help detect power status\",\n          \"toggle_art_mode\": \"Power button switch to art mode (Frame TV only)\"\n        }\n      },\n      \"sync_ent\": {\n        \"title\": \"SamsungTV Smart synched entities\",\n        \"data\": {\n          \"sync_turn_off\": \"List of entities to Power OFF with TV\",\n          \"sync_turn_on\": \"List of entities to power ON with TV\"\n        }\n      },\n      \"app_list\": {\n        \"title\": \"SamsungTV Smart applications list configuration\",\n        \"data\": {\n          \"app_list\": \"Applications list:\"\n        }\n      },\n      \"channel_list\": {\n        \"title\": \"SamsungTV Smart channels list configuration\",\n        \"data\": {\n          \"channel_list\": \"Channels list:\"\n        }\n      },\n      \"source_list\": {\n        \"title\": \"SamsungTV Smart sources list configuration\",\n        \"data\": {\n          \"source_list\": \"Sources list:\"\n        }\n      }\n    },\n    \"error\": {\n      \"invalid_tv_list\": \"Invalid format. Please check documentation\"\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/samsungtv_smart/translations/hu.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Ez a Samsung TV már konfigurálva van.\",\n      \"already_in_progress\": \"A Samsung TV konfigurációja már folyamatban van.\",\n      \"unsupported_version\": \"Ehhez az integrációhoz legalább a HomeAssistant {req_ver} verzió szükséges, jelenleg a(z) {run_ver} verziót használja.\"\n    },\n    \"error\": {\n      \"auth_missing\": \"A Home Assistantnek nincs engedélye a ehhez a Samsung TV-hez való kapcsolódáshoz. Kérjük, ellenőrizze a TV beállításait, hogy engedélyezni tudja a Home Assistant számára.\",\n      \"invalid_host\": \"Érvénytelen hoszt.\",\n      \"not_successful\": \"Nem sikerült létrehozni WebSocket kapcsolatot ezzel a Samsung TV készülékkel.\",\n      \"not_supported\": \"Ez a Samsung TV készülék jelenleg nem támogatott.\",\n      \"wrong_api_key\": \"Hibás SmartThings token.\",\n      \"st_device_not_found\": \"SmartThings TV eszközazonosító (deviceID) nem található.\",\n      \"st_device_used\": \"SmartThings TV eszközazonosító (deviceID) már használatban van.\"\n    },\n    \"flow_title\": \"SamsungTV Smart: {model}\",\n    \"step\": {\n      \"confirm\": {\n        \"description\": \"Szeretné beállítani a Samsung TV-t ({model})? Ha még soha nem csatlakoztatta a Home Assistantot, akkor megjelenik egy felugró ablak a TV-n, ami engedélyezést kér. A manuális konfigurációk ezen TV esetében felülíródnak.\"\n      },\n      \"user\": {\n        \"data\": {\n          \"host\": \"Hoszt vagy IP cím\",\n          \"name\": \"Az entitásnak adott név\",\n          \"use_ha_name_for_ws\": \"Használja a HA példány nevét az azonosításhoz a TV-n\",\n          \"api_key\": \"SmartThings által generált token (opcionális)\"\n        },\n        \"description\": \"Adja meg a Samsung TV adataidat. A SmartThings token opcionális, de erősen javasolt.\\nMiután megerősítette, látnia kell egy felugró ablakot a TV-n, ami engedélyezést kér.\"\n      },\n      \"stdevice\": {\n        \"data\": {\n          \"st_devices\": \"SmartThings TV\"\n        },\n        \"description\": \"Több TV-t is konfigurált a fiókján. Válassza ki a konfigurálandó TV-t a listából.\"\n      },\n      \"stdeviceid\": {\n        \"data\": {\n          \"device_id\": \"SmartThings TV eszközazonosító (deviceID)\"\n        },\n        \"description\": \"Az automatikus SmartThings eszközazonosítás sikertelen volt. A folytatáshoz az eszközazonosítót be kell azonosítania a SmartThings weboldalon (bővebben a dokumentációban) és be kell illesztenie ide.\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"title\": \"SamsungTV Smart beállítások\",\n        \"data\": {\n          \"use_st_status_info\": \"Használja a SmartThings TV állapotinformációt\",\n          \"use_st_channel_info\": \"Használja a SmartThings TV csatornainformációt\",\n          \"show_channel_number\": \"Használja a SmartThings TV csatornaszám információt\",\n          \"app_load_method\": \"Alkalmazások lista betöltési módja induláskor\",\n          \"logo_option\": \"Mutassa a logót ismert forrásokhoz, alkalmazásokhoz és csatornákhoz\",\n          \"use_local_logo\": \"Engedélyezze a helyi logóképek használatát\",\n          \"power_on_method\": \"A TV bekapcsolásához használt módszer\",\n          \"show_adv_opt\": \"Opciók menü mutatása\"\n        }\n      },\n      \"menu\": {\n        \"title\": \"SamsungTV Smart opciók menü\",\n        \"menu_options\": {\n          \"adv_opt\": \"Haladó opciók\",\n          \"app_list\": \"Alkalmazások listájának konfigurálása\",\n          \"channel_list\": \"Csatornák listájának konfigurálása\",\n          \"init\": \"Alapbeállítások\",\n          \"save_exit\": \"Opciók mentése és kilépés\",\n          \"source_list\": \"Források listájának konfigurálása\",\n          \"sync_ent\": \"Szinkronizált entitások konfigurálása\"\n        }\n      },\n      \"adv_opt\": {\n        \"title\": \"SamsungTV Smart haladó opciók\",\n        \"data\": {\n          \"app_launch_method\": \"Az alkalmazások indítási módszerének használata\",\n          \"dump_apps\": \"Az alkalmazások listájának naplófájlba való kiírása induláskor (amennyiben lehetséges)\",\n          \"use_mute_check\": \"Hangerőnémítás állapotának használata hamis bekapcsolás észleléséhez\",\n          \"wol_repeat\": \"WOL (Wake-on-LAN) csomag ismétlései a TV bekapcsolásához\",\n          \"power_on_delay\": \"Másodpercek a bekapcsolási állapot késleltetéséhez\",\n          \"ping_port\": \"TCP port, amelyet a bekapcsolási állapot ellenőrzéséhez használnak (0 az ICMP-hez)\",\n          \"ext_power_entity\": \"Bemeneti érzékelő a bekapcsolási állapot felismeréséhez\",\n          \"toggle_art_mode\": \"Art módra váltó bekapcsológomb (kizárólag Frame TV esetén)\"\n        }\n      },\n      \"sync_ent\": {\n        \"title\": \"SamsungTV Smart szinkronizált entitások\",\n        \"data\": {\n          \"sync_turn_off\": \"Lista azokról az entitásokról, amelyeket kikapcsol a TV-vel\",\n          \"sync_turn_on\": \"Lista azokról az entitásokról, amelyeket bekapcsol a TV-vel\"\n        }\n      },\n      \"app_list\": {\n        \"title\": \"SamsungTV Smart alkalmazások listájának konfigurálása\",\n        \"data\": {\n          \"app_list\": \"Alkalmazások listája:\"\n        }\n      },\n      \"channel_list\": {\n        \"title\": \"SamsungTV Smart csatornák listájának konfigurálása\",\n        \"data\": {\n          \"channel_list\": \"Csatornák listája:\"\n        }\n      },\n      \"source_list\": {\n        \"title\": \"SamsungTV Smart források listájának konfigurálása\",\n        \"data\": {\n          \"source_list\": \"Források listája:\"\n        }\n      }\n    },\n    \"error\": {\n      \"invalid_tv_list\": \"Érvénytelen formátum. Kérjük, ellenőrizze a dokumentációt.\"\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/samsungtv_smart/translations/it.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Questo Samsung TV \\u00e8 gi\\u00e0 configurato.\",\n      \"already_in_progress\": \"La configurazione di Samsung TV \\u00e8 gi\\u00e0 in corso.\",\n      \"host_unique_id\": \"Riconfigurazione non possibile poichè il nome host è usato come unique id. Rimuovere questa entry e configurarne una nuova.\",\n      \"reconfigure_successful\": \"Riconfigurazione Samsung TV completata correttamente.\",\n      \"unsupported_version\": \"Questa integrazione richiede almeno la versione {req_ver} di HomeAssistant, tu stai usando la versione {run_ver}.\"\n    },\n    \"error\": {\n      \"auth_missing\": \"Home Assistant non \\u00e8 autorizzato a connettersi a questo Samsung TV. Controlla le impostazioni del tuo TV per autorizzare Home Assistant.\",\n      \"invalid_host\": \"Nome host non valido.\",\n      \"not_successful\": \"Impossibile aprire la connessione WebSocket con questo dispositivo Samsung TV.\",\n      \"not_supported\": \"Questo dispositivo Samsung TV non \\u00e8 attualmente supportato.\",\n      \"only_key_or_st\": \"Specificare solo il token di accesso o l'entry SmartThings.\",\n      \"st_api_key_fail\": \"Errore nel recuperare il token di accesso dall'entry SmartThings selezionata.\",\n      \"st_device_not_found\": \"SmartThings TV deviceID non trovato.\",\n      \"st_device_used\": \"SmartThings TV deviceID gi\\u00e0 utilizzato.\",\n      \"wrong_api_key\": \"SmartThings token errato.\"\n    },\n    \"flow_title\": \"SamsungTV Smart: {model}\",\n    \"step\": {\n      \"confirm\": {\n        \"description\": \"Vuoi configurare Samsung TV {model}? Se non hai mai connesso Home Assistant in precedenza, dovresti vedere un messaggio sul tuo TV in cui \\u00e8 richiesta l'autorizzazione. Le configurazioni manuali per questo TV verranno sovrascritte.\"\n      },\n      \"user\": {\n        \"data\": {\n          \"host\": \"Host o indirizzo IP\",\n          \"name\": \"Nome dell'entit\\u00e0\",\n          \"use_ha_name_for_ws\": \"Usare il nome dell'istanza HA per identificarsi sulla TV\",\n          \"api_key\": \"SmartThings token generato (opzionale)\",\n          \"st_entry_unique_id\": \"Entry SmartThings usata per fornire le credenziali SmartThings\"\n        },\n        \"description\": \"Inserisci le informazioni del tuo Samsung TV. L'uso di SmartThings \\u00e8 opzionale ma fortemente consigliato.\\nDopo aver confermato i dati, dovresti vedere un messaggio sul TV in cui \\u00e8 richiesta l'autorizzazione.\"\n      },\n      \"reconfigure\": {\n        \"data\": {\n          \"host\": \"Host o indirizzo IP\",\n          \"api_key\": \"SmartThings token generato (opzionale, se vuoto verrà usato quello esistente)\",\n          \"st_entry_unique_id\": \"Entry SmartThings usata per fornire le credenziali SmartThings\"\n        },\n        \"description\": \"Inserisci le informazioni da aggiornare del tuo Samsung TV. Le configurazioni SmartThings possono essere cambiate ma non rimosse.\\nDopo aver confermato i dati, potresti vedere un messaggio sul TV in cui \\u00e8 richiesta l'autorizzazione.\"\n      },\n      \"stdevice\": {\n        \"data\": {\n          \"st_devices\": \"SmartThings TV\"\n        },\n        \"description\": \"Hai più di un TV configurato sul tuo account. Seleziona il TV che stai configurando dalla lista.\"\n      },\n      \"stdeviceid\": {\n        \"data\": {\n          \"device_id\": \"SmartThings TV deviceID\"\n        },\n        \"description\": \"Identificazione automatica del deviceID SmartThings fallita. Per continuare devi identificare il deviceID sul sito SmartThings (vedere documentazione) e inserirlo qui.\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"title\": \"Opzioni SamsungTV Smart\",\n        \"data\": {\n          \"use_st_status_info\": \"Usa informazioni Stato TV da SmartThings\",\n          \"use_st_channel_info\": \"Usa informazioni Canale TV da SmartThings\",\n          \"show_channel_number\": \"Usa informazioni Numero Canale TV da SmartThings\",\n          \"app_load_method\": \"Modalità caricamento lista applicazioni all'avvio\",\n          \"logo_option\": \"Visualizza il logo per sorgenti, apps e canali conosciuti\",\n          \"use_local_logo\": \"Permetti l'uso delle immagini logo locali\",\n          \"power_on_method\": \"Metodo usato per accendere la TV\",\n          \"show_adv_opt\": \"Mostra menu opzioni\"\n        }\n      },\n      \"menu\": {\n        \"title\": \"Menù opzioni SamsungTV Smart\",\n        \"menu_options\": {\n          \"adv_opt\": \"Opzioni avanzate\",\n          \"app_list\": \"Configurazione lista applicazioni\",\n          \"channel_list\": \"Configurazione lista canali\",\n          \"init\": \"Opzioni standard\",\n          \"save_exit\": \"Salva le opzioni ed esci\",\n          \"source_list\": \"Configurazione lista sorgenti\",\n          \"sync_ent\": \"Configurazione entità collegate\"\n        }\n      },\n      \"adv_opt\": {\n        \"title\": \"Opzioni avanzate SamsungTV Smart\",\n        \"data\": {\n          \"app_launch_method\": \"Metodo usato per lanciare le applicazioni\",\n          \"dump_apps\": \"Mostra lista apps nel file di log all'avvio (se possibile)\",\n          \"use_mute_check\": \"Utilizza lo stato di volume muto per identificare false accensioni\",\n          \"wol_repeat\": \"Numero di volte che il pacchetto WOL viene inviato per accendere la TV\",\n          \"power_on_delay\": \"Secondi di ritardo per passaggio allo stato ON\",\n          \"ping_port\": \"Porta TCP usata per identificare lo stato (0 per ICMP)\",\n          \"ext_power_entity\": \"Binary sensor usato per aiutare a identificare lo stato\",\n          \"toggle_art_mode\": \"Pulsante di accensione passa a art mode (solo per Frame TV)\"\n        }\n      },\n      \"sync_ent\": {\n        \"title\": \"Entità collegate SamsungTV Smart\",\n        \"data\": {\n          \"sync_turn_off\": \"Elenco entit\\u00e0 da Spegnere con la TV\",\n          \"sync_turn_on\": \"Elenco entit\\u00e0 da Accendere con la TV\"\n        }\n      },\n      \"app_list\": {\n        \"title\": \"Configurazione lista applicazioni SamsungTV Smart\",\n        \"data\": {\n          \"app_list\": \"Lista applicazioni:\"\n        }\n      },\n      \"channel_list\": {\n        \"title\": \"Configurazione lista canali SamsungTV Smart\",\n        \"data\": {\n          \"channel_list\": \"Lista canali:\"\n        }\n      },\n      \"source_list\": {\n        \"title\": \"Configurazione lista sorgenti SamsungTV Smart\",\n        \"data\": {\n          \"source_list\": \"Lista sorgenti:\"\n        }\n      }\n    },\n    \"error\": {\n      \"invalid_tv_list\": \"Formato not valido. Controlla la documentazione\"\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/samsungtv_smart/translations/pt-BR.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Essa TV Samsung já está configurada.\",\n      \"already_in_progress\": \"A configuração da TV Samsung já está em andamento.\",\n      \"unsupported_version\": \"Esta integração requer pelo menos a versão do HomeAssistant {req_ver}, você está executando a versão {run_ver}.\"\n    },\n    \"error\": {\n      \"auth_missing\": \"O Home Assistant não está autorizado a se conectar a essa TV Samsung. Verifique as configurações da sua TV para autorizar o Home Assistant.\",\n      \"invalid_host\": \"Host inválido.\",\n      \"not_successful\": \"Não é possível criar uma conexão WebSocket com essa TV Samsung.\",\n      \"not_supported\": \"Essa TV Samsung não é compatível no momento.\",\n      \"wrong_api_key\": \"Token errado do SmartThings.\",\n      \"st_device_not_found\": \"ID de TV SmartThings não encontrado.\",\n      \"st_device_used\": \"ID de TV SmartThings já está em uso.\"\n    },\n    \"flow_title\": \"SamsungTV Smart: {model}\",\n    \"step\": {\n      \"confirm\": {\n        \"description\": \"Deseja configurar a TV Samsung {model}? Se você nunca conectou o Home Assistant antes, verá um pop-up na TV solicitando uma autorização. As configurações manuais para esta TV serão substituídas.\"\n      },\n      \"user\": {\n        \"data\": {\n          \"host\": \"Host ou endereço IP\",\n          \"name\": \"Nome atribuído à entidade\",\n          \"use_ha_name_for_ws\": \"Use o nome da instância do HA para a identificação na TV\",\n          \"api_key\": \"Token gerado pelo SmartThings (opcional)\"\n        },\n        \"description\": \"Insira as informações da sua TV Samsung.O token SmartThings é opcional, mas muito sugerido.\\nDepois de confirmar os dados, verá um pop-up na TV solicitando uma autorização.\"\n      },\n      \"stdevice\": {\n        \"data\": {\n          \"st_devices\": \"SmartThings TV\"\n        },\n        \"description\": \"Você tem várias TVs configuradas em sua conta. Selecione a TV que você está configurando na lista.\"\n      },\n      \"stdeviceid\": {\n        \"data\": {\n          \"device_id\": \"ID de TV SmartThings\"\n        },\n        \"description\": \"Falha na detecção automática de ID do SmartThings. Para continuar você deve identificar o ID no site SmartThings (ver documentação) e inseri-lo aqui.\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"title\": \"Opções SamsungTV Smart\",\n        \"data\": {\n          \"use_st_status_info\": \"Use as informações de status da TV SmartThings\",\n          \"use_st_channel_info\": \"Use as informações dos canais de TV SmartThings\",\n          \"show_channel_number\": \"Use as informações de número dos canais de TV SmartThings\",\n          \"app_load_method\": \"Modo de carregamento da lista de aplicativos na inicialização\",\n          \"logo_option\": \"Exiba uma logo para fontes, aplicativos e canais conhecidos\",\n          \"use_local_logo\": \"Permitir o uso de imagens de logotipos locais\",\n          \"power_on_method\": \"Método usado para ligar a TV\",\n          \"show_adv_opt\": \"Mostrar menu opções\"\n        }\n      },\n      \"adv_opt\": {\n        \"title\": \"Opções avançadas SamsungTV Smart\",\n        \"data\": {\n          \"app_launch_method\": \"Método usado na inicialização de aplicativos\",\n          \"dump_apps\": \"Despejar a lista de aplicativos no arquivo de log na inicialização (quando possível)\",\n          \"use_mute_check\": \"Use o status de volume mudo para detectar um falso status de LIGADO\",\n          \"wol_repeat\": \"Número de tempo que o pacote WOL é enviado para ligar a TV\",\n          \"power_on_delay\": \"Segundos de delay para o status LIGADO\",\n          \"ping_port\": \"Porta TCP usada para verificar o status ligado/desligado (0 para ICMP)\",\n          \"ext_power_entity\": \"Binary sensor para ajudar a detectar o status de energia\"\n        }\n      },\n      \"sync_ent\": {\n        \"title\": \"SamsungTV Smart synched entities\",\n        \"data\": {\n          \"sync_turn_off\": \"Lista de entidades para desligar com a TV\",\n          \"sync_turn_on\": \"Lista de entidades para ligar com a TV\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "docs/App_list.md",
    "content": "# HomeAssistant - SamsungTV Smart Component\n\n***app_list guide***\n---------------\n\n**Note:** Although this is an optional value, **it is highly recommended to set it manually**, even if in some (rare) cases the app list can be gotten from the TV successfully.\n\nThe `app_list` is used to set apps that you have installed on your TV. The app names can be associated with 2 types of IDs that Samsung TVs support: numerical IDs and alphanumerical IDs.\n\nAn application normally has both a numerical ID and an alphanumerical ID associated with it.\n\nHere are some known lists of app IDs: [List 1](https://github.com/tavicu/homebridge-samsung-tizen/issues/26#issuecomment-447424879), [List 2](https://github.com/Ape/samsungctl/issues/75#issuecomment-404941201)\n\n(Another way of finding the alphanumerical ID is by enabling the [SmartThings API](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md) and running an app on the TV, this will show the alphanumerical ID as `media_title` in the component)\n\nHere are 3 examples values for `app_list`:\n- `'{\"Netflix\": \"11101200001\", \"Prime Video\": \"3201512006785\", \"Spotify\": \"3201606009684\"}'`\n- `'{\"Netflix\": \"org.tizen.netflix-app\", \"Prime Video\": \"org.tizen.ignition\", \"Spotify\": \"3201606009684\"}'`\n- `'{\"Netflix\": \"11101200001/org.tizen.netflix-app\", \"Prime Video\": \"3201512006785/org.tizen.ignition\", \"Spotify\": \"3201606009684\"}'`\n\n(the last one is the prefered method, which includes both numerical and alphanumerical IDs, for increased support of this component)\n\nIn order to understand these example values, you must first understand what these IDs are used for.\n\nAn app ID is used to start the app on the TV and also to identify the running app on the TV.\n\nTo run the app on the TV, both numerical and alphanumerical IDs can be used.\n\nTo get the running app from the TV, two different ways are used:\n- one works only with numerical IDs by doing HTTP polling on the TV (1 request for each app in `app_list`), this is a lengthy task that should be avoided if possible (this can be completely avoided by setting `scan_app_http` to `False` in your component's config)\n- another works only with the alphanumerical IDs by getting the running app from the SmartThings API (requires SmartThings API enabled in your component's config)\n\n**Note:** There is one rare case, for a few apps (like \"Prime Video\") where the numerical ID will work to start the app, but not to identify the running app, while it's alphanumerical ID will work to get the running app but not run the app. It is for this case that we also allow setting both numerical and alphanumerical IDs at the same time (separated by \"/\") which will allow this component to correctly handle this rare case too.\n\n"
  },
  {
    "path": "docs/Key_chaining.md",
    "content": "# HomeAssistant - SamsungTV Smart Component\n\n***Key Chaining Patterns***\n---------------\n\n**Note:** If SmartThings API was enabled by setting `api_key` and `device_id`, then these codes are also supported: `ST_TV`, `ST_HDMI1`, `ST_HDMI2`, `ST_HDMI3`, etc. which will change the input source faster then key chaining patterns will\n\nKey codes can be chained with the \"+\" symbol, delays can also be set in milliseconds (200 to 2000, default=500) between the \"+\" symbols.\n\nThis is a list of known and tested key chaining patterns. To see the complete list of known key codes, [check this list](./Key_codes.md)\n\n\n**Switch to Live TV**\n\n`KEY_EXIT+2000+KEY_TV+KEY_EXIT`\n\n\n**Switch to first Source in List (2019 TV)**\n\n`KEY_SOURCE+KEY_DOWN+KEY_UP+KEY_LEFT+KEY_LEFT+KEY_LEFT+KEY_ENTER`\n\n\n**Switch to second Source in List (2019 TV)**\n\n`KEY_SOURCE+KEY_DOWN+KEY_UP+KEY_LEFT+KEY_LEFT+KEY_LEFT+KEY_RIGHT+KEY_ENTER`\n\n\n**Switch to third Source in List (2019 TV)**\n\n`KEY_SOURCE+KEY_DOWN+KEY_UP+KEY_LEFT+KEY_LEFT+KEY_LEFT+KEY_RIGHT+KEY_RIGHT+KEY_ENTER`\n\n\n**Switch to forth Source in List (2019 TV)**\n\n`KEY_SOURCE+KEY_DOWN+KEY_UP+KEY_LEFT+KEY_LEFT+KEY_LEFT+KEY_RIGHT+KEY_RIGHT+KEY_RIGHT+KEY_ENTER`\n\n\n**Switch to first Source in List (2017 TV)**\n\n`KEY_SOURCE+KEY_LEFT+KEY_LEFT+KEY_LEFT+KEY_ENTER`\n\n\n**Switch to second Source in List (2017 TV)**\n\n`KEY_SOURCE+KEY_LEFT+KEY_LEFT+KEY_LEFT+KEY_RIGHT+KEY_ENTER`\n\n\n**Switch to third Source in List (2017 TV)**\n\n`KEY_SOURCE+KEY_LEFT+KEY_LEFT+KEY_LEFT+KEY_RIGHT+KEY_RIGHT+KEY_ENTER`\n\n\n**Switch to forth Source in List (2017 TV)**\n\n`KEY_SOURCE+KEY_LEFT+KEY_LEFT+KEY_LEFT+KEY_RIGHT+KEY_RIGHT+KEY_RIGHT+KEY_ENTER`\n\n"
  },
  {
    "path": "docs/Key_codes.md",
    "content": "# HomeAssistant - SamsungTV Smart Component\n\n***Key Codes***\n---------------\nIf [SmartThings API](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md) was enabled by setting `api_key`, then these codes are also supported: `ST_TV`, `ST_HDMI1`, `ST_HDMI2`, `ST_HDMI3`, etc. (see the [entire list of SmartThings keys](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md#smartthings-keys))\n\nThe list of accepted keys may vary depending on the TV model, but the following list has some common key codes and their descriptions.\n\n*Power Keys*\n____________\nKey|Description\n---|-----------\nKEY_POWEROFF|PowerOFF\nKEY_POWERON|PowerOn\nKEY_POWER|PowerToggle\n\n*Input Keys*\n____________\nKey|Description\n---|-----------\nKEY_SOURCE|Source\nKEY_COMPONENT1|Component1\nKEY_COMPONENT2|Component2\nKEY_AV1|AV1\nKEY_AV2|AV2\nKEY_AV3|AV3\nKEY_SVIDEO1|SVideo1\nKEY_SVIDEO2|SVideo2\nKEY_SVIDEO3|SVideo3\nKEY_HDMI|HDMI\nKEY_FM_RADIO|FMRadio\nKEY_DVI|DVI\nKEY_DVR|DVR\nKEY_TV|TV\nKEY_ANTENA|AnalogTV\nKEY_DTV|DigitalTV\nKEY_AMBIENT|AmbientMode\n\n*Number Keys*\n_____________\nKey|Description\n---|-----------\nKEY_1|Key1\nKEY_2|Key2\nKEY_3|Key3\nKEY_4|Key4\nKEY_5|Key5\nKEY_6|Key6\nKEY_7|Key7\nKEY_8|Key8\nKEY_9|Key9\nKEY_0|Key0\n\n*Misc Keys*\n___________\nKey|Description\n---|-----------\nKEY_PANNEL_CHDOWN|3D\nKEY_ANYNET|AnyNet+\nKEY_ESAVING|EnergySaving\nKEY_SLEEP|SleepTimer\nKEY_DTV_SIGNAL|DTVSignal\n\n*Channel Keys*\n______________\nKey|Description\n---|-----------\nKEY_CHUP|ChannelUp\nKEY_CHDOWN|ChannelDown\nKEY_PRECH|PreviousChannel\nKEY_FAVCH|FavoriteChannels\nKEY_CH_LIST|ChannelList\nKEY_AUTO_PROGRAM|AutoProgram\nKEY_MAGIC_CHANNEL|MagicChannel\n\n*Volume Keys*\n_____________\nKey|Description\n---|-----------\nKEY_VOLUP|VolumeUp\nKEY_VOLDOWN|VolumeDown\nKEY_MUTE|Mute\n\n*Direction Keys*\n________________\nKey|Description\n---|-----------\nKEY_UP|NavigationUp\nKEY_DOWN|NavigationDown\nKEY_LEFT|NavigationLeft\nKEY_RIGHT|NavigationRight\nKEY_RETURN|NavigationReturn/Back\nKEY_ENTER|NavigationEnter\nKEY_EXIT|NavigationExit\n\n*Media Keys*\n____________\nKey|Description\n---|-----------\nKEY_REWIND|Rewind\nKEY_STOP|Stop\nKEY_PLAY|Play\nKEY_FF|FastForward\nKEY_REC|Record\nKEY_PAUSE|Pause\nKEY_LIVE|Live\nKEY_QUICK_REPLAY|fnKEY_QUICK_REPLAY\nKEY_STILL_PICTURE|fnKEY_STILL_PICTURE\nKEY_INSTANT_REPLAY|fnKEY_INSTANT_REPLAY\n\n*Picture in Picture*\n____________________\nKey|Description\n---|-----------\nKEY_PIP_ONOFF|PIPOn/Off\nKEY_PIP_SWAP|PIPSwap\nKEY_PIP_SIZE|PIPSize\nKEY_PIP_CHUP|PIPChannelUp\nKEY_PIP_CHDOWN|PIPChannelDown\nKEY_AUTO_ARC_PIP_SMALL|PIPSmall\nKEY_AUTO_ARC_PIP_WIDE|PIPWide\nKEY_AUTO_ARC_PIP_RIGHT_BOTTOM|PIPBottomRight\nKEY_AUTO_ARC_PIP_SOURCE_CHANGE|PIPSourceChange\nKEY_PIP_SCAN|PIPScan\n\n*Modes*\n_______\nKey|Description\n---|-----------\nKEY_VCR_MODE|VCRMode\nKEY_CATV_MODE|CATVMode\nKEY_DSS_MODE|DSSMode\nKEY_TV_MODE|TVMode\nKEY_DVD_MODE|DVDMode\nKEY_STB_MODE|STBMode\nKEY_PCMODE|PCMode\n\n*Color Keys*\n____________\nKey|Description\n---|-----------\nKEY_GREEN|Green\nKEY_YELLOW|Yellow\nKEY_CYAN|Cyan\nKEY_RED|Red\nKEY_COLOR|Color selection\n\n*Teletext*\n__________\nKey|Description\n---|-----------\nKEY_TTX_MIX|TeletextMix\nKEY_TTX_SUBFACE|TeletextSubface\n\n*AspectRatio*\n______________\nKey|Description\n---|-----------\nKEY_ASPECT|AspectRatio\nKEY_PICTURE_SIZE|PictureSize\nKEY_4_3|AspectRatio4:3\nKEY_16_9|AspectRatio16:9\nKEY_EXT14|AspectRatio3:4(Alt)\nKEY_EXT15|AspectRatio16:9(Alt)\n\n*Picture Mode*\n______________\nKey|Description\n---|-----------\nKEY_PMODE|PictureMode\nKEY_PANORAMA|PictureModePanorama\nKEY_DYNAMIC|PictureModeDynamic\nKEY_STANDARD|PictureModeStandard\nKEY_MOVIE1|PictureModeMovie\nKEY_GAME|PictureModeGame\nKEY_CUSTOM|PictureModeCustom\nKEY_EXT9|PictureModeMovie(Alt)\nKEY_EXT10|PictureModeStandard(Alt)\n\n*Menus*\n_______\nKey|Description\n---|-----------\nKEY_MENU|Menu\nKEY_TOPMENU|TopMenu\nKEY_TOOLS|Tools\nKEY_HOME|Home\nKEY_CONTENTS|Contents\nKEY_GUIDE|Guide\nKEY_DISC_MENU|DiscMenu\nKEY_DVR_MENU|DVRMenu\nKEY_HELP|Help\n\n*OSD*\n_____\nKey|Description\n---|-----------\nKEY_INFO|Info\nKEY_CAPTION|Caption\nKEY_CLOCK_DISPLAY|ClockDisplay\nKEY_SETUP_CLOCK_TIMER|SetupClock\nKEY_SUB_TITLE|Subtitle\n\n*Zoom*\n______\nKey|Description\n---|-----------\nKEY_ZOOM_MOVE|ZoomMove\nKEY_ZOOM_IN|ZoomIn\nKEY_ZOOM_OUT|ZoomOut\nKEY_ZOOM1|Zoom1\nKEY_ZOOM2|Zoom2\n\n*Other Keys*\n____________\nKey|Description\n---|-----------\nKEY_WHEEL_LEFT|WheelLeft\nKEY_WHEEL_RIGHT|WheelRight\nKEY_ADDDEL|Add/Del\nKEY_PLUS100|Plus100\nKEY_AD|AD\nKEY_LINK|Link\nKEY_TURBO|Turbo\nKEY_CONVERGENCE|Convergence\nKEY_DEVICE_CONNECT|DeviceConnect\nKEY_11|Key11\nKEY_12|Key12\nKEY_FACTORY|KeyFactory\nKEY_3SPEED|Key3SPEED\nKEY_RSURF|KeyRSURF\nKEY_FF_|FF_\nKEY_REWIND_|REWIND_\nKEY_ANGLE|Angle\nKEY_RESERVED1|Reserved1\nKEY_PROGRAM|Program\nKEY_BOOKMARK|Bookmark\nKEY_PRINT|Print\nKEY_CLEAR|Clear\nKEY_VCHIP|VChip\nKEY_REPEAT|Repeat\nKEY_DOOR|Door\nKEY_OPEN|Open\nKEY_DMA|DMA\nKEY_MTS|MTS\nKEY_DNIe|DNIe\nKEY_SRS|SRS\nKEY_CONVERT_AUDIO_MAINSUB|ConvertAudioMain/Sub\nKEY_MDC|MDC\nKEY_SEFFECT|SoundEffect\nKEY_PERPECT_FOCUS|PERPECTFocus\nKEY_CALLER_ID|CallerID\nKEY_SCALE|Scale\nKEY_MAGIC_BRIGHT|MagicBright\nKEY_W_LINK|WLink\nKEY_DTV_LINK|DTVLink\nKEY_APP_LIST|ApplicationList\nKEY_BACK_MHP|BackMHP\nKEY_ALT_MHP|AlternateMHP\nKEY_DNSe|DNSe\nKEY_RSS|RSS\nKEY_ENTERTAINMENT|Entertainment\nKEY_ID_INPUT|IDInput\nKEY_ID_SETUP|IDSetup\nKEY_ANYVIEW|AnyView\nKEY_MS|MS\nKEY_MORE|\nKEY_MIC|\nKEY_NINE_SEPERATE|\nKEY_AUTO_FORMAT|AutoFormat\nKEY_DNET|DNET\nKEY_EXTRA|RemoteAccess\n\n*Auto Arc Keys*\n_______________\nKey|Description\n---|-----------\nKEY_AUTO_ARC_C_FORCE_AGING|\nKEY_AUTO_ARC_CAPTION_ENG|\nKEY_AUTO_ARC_USBJACK_INSPECT|\nKEY_AUTO_ARC_RESET|\nKEY_AUTO_ARC_LNA_ON|\nKEY_AUTO_ARC_LNA_OFF|\nKEY_AUTO_ARC_ANYNET_MODE_OK|\nKEY_AUTO_ARC_ANYNET_AUTO_START|\nKEY_AUTO_ARC_CAPTION_ON|\nKEY_AUTO_ARC_CAPTION_OFF|\nKEY_AUTO_ARC_PIP_DOUBLE|\nKEY_AUTO_ARC_PIP_LARGE|\nKEY_AUTO_ARC_PIP_LEFT_TOP|\nKEY_AUTO_ARC_PIP_RIGHT_TOP|\nKEY_AUTO_ARC_PIP_LEFT_BOTTOM|\nKEY_AUTO_ARC_PIP_CH_CHANGE|\nKEY_AUTO_ARC_AUTOCOLOR_SUCCESS|\nKEY_AUTO_ARC_AUTOCOLOR_FAIL|\nKEY_AUTO_ARC_JACK_IDENT|\nKEY_AUTO_ARC_CAPTION_KOR|\nKEY_AUTO_ARC_ANTENNA_AIR|\nKEY_AUTO_ARC_ANTENNA_CABLE|\nKEY_AUTO_ARC_ANTENNA_SATELLITE|\n\n*Panel Keys*\n____________\nKey|Description\n---|-----------\nKEY_PANNEL_POWER|\nKEY_PANNEL_CHUP|\nKEY_PANNEL_VOLUP|\nKEY_PANNEL_VOLDOW|\nKEY_PANNEL_ENTER|\nKEY_PANNEL_MENU|\nKEY_PANNEL_SOURCE|\nKEY_PANNEL_ENTER|\n\n<br></br>\n*Extended Keys*\n_______________\nKey|Description\n---|-----------\nKEY_EXT1|\nKEY_EXT2|\nKEY_EXT3|\nKEY_EXT4|\nKEY_EXT5|\nKEY_EXT6|\nKEY_EXT7|\nKEY_EXT8|\nKEY_EXT11|\nKEY_EXT12|\nKEY_EXT13|\nKEY_EXT16|\nKEY_EXT17|\nKEY_EXT18|\nKEY_EXT19|\nKEY_EXT20|\nKEY_EXT21|\nKEY_EXT22|\nKEY_EXT23|\nKEY_EXT24|\nKEY_EXT25|\nKEY_EXT26|\nKEY_EXT27|\nKEY_EXT28|\nKEY_EXT29|\nKEY_EXT30|\nKEY_EXT31|\nKEY_EXT32|\nKEY_EXT33|\nKEY_EXT34|\nKEY_EXT35|\nKEY_EXT36|\nKEY_EXT37|\nKEY_EXT38|\nKEY_EXT39|\nKEY_EXT40|\nKEY_EXT41|\n\nPlease note that some codes are different on the 2016+ TVs. For example, `KEY_POWEROFF` is `KEY_POWER` on the newer TVs.\n\n***References***\n----------------\n\nThe code list has been extracted from: https://github.com/kdschlosser/samsungctl\n"
  },
  {
    "path": "docs/Smartthings.md",
    "content": "# HomeAssistant - SamsungTV Smart Integration\n\n## ***Enable SmartThings*** - Setup instructions\n\n### SmartThings authentication\n\nTo use SmartThings feature in integration you must provide authentication information. There are 2 way to do this.\n\n#### Method 1: Use native SmartThings integration (suggested)\n\nConfigure on your HA instance the [native HA SmartThings integration](https://www.home-assistant.io/integrations/smartthings/). In this way the API key to access to SmartThings will be automatically provided to `Samsung TV Smart` integration and you don't have to do any other steps. Just remenber to select the `SmartThings entry used to provide SmartThings credential` in Samsung TV Smart configuration flow.\n\n\n#### Method 2: Create personal access token (deprecated)\n\n1. Log into the [personal access tokens page](https://account.smartthings.com/tokens) and click '[Generate new token](https://account.smartthings.com/tokens/new)'\n2. Enter a token name (can be whatever you want), for example, 'Home Assistant' and select the following authorized scopes:\n    - Devices (all)\n    - Installed Applications (all)\n    - Scenes (all)\n    - Applications (all)\n    - Locations (all)\n    - Schedules (all)\n\n3. Click 'Generate token'. When the token is displayed, copy and save it somewhere safe (such as your keystore) as you will not be able to retrieve it again.\n\n**Note:** starting from 30 December 2024 generated `personal access token (PAT)` have a duration of 24 hours as explained [here](https://developer.smartthings.com/docs/getting-started/authorization-and-permissions). For this reason use of `PAT` is not recommended because you should manually update your token every 24 hours. In case you can use the integration reconfigure option to update it.<br/>\n\n### Configure Home Assistant\n\nOnce the SmartThings token has been generated, you need to configure the integration with it in order to make it work as explained in the main guide. If you previously\nconfigured the [native HA SmartThings integration](https://www.home-assistant.io/integrations/smartthings/), remenber to select the `SmartThings entry used to provide SmartThings credential` during configuration flow.\n\n**Note:** if the integration has been already configured for your TV, you must delete it from the HA web interface and then re-configure it to enable SmartThings integration.<br/>\n\n#### SmartThings Device ID\n\nIf during configuration flow automatic detection of SmartThings device ID fails, a new configuration page will open requesting you to manual\ninsert it.\nTo identify your TV device ID use the following steps:\n\n- Go [here](https://my.smartthings.com/advanced/devices) and login with your SmartThings credential\n- Click on the name of your TV from the list of available devices\n- In the new page search the column called `Device ID`\n- Copy the value (is a UUID code) and paste it in the HomeAssistant configuration page\n\n\n\n***Benefits of Enabling SmartThings***\n---------------\n\n- Better states for running apps (read [app_list guide](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/App_list.md) for more information)\n- New keys available (read more below about [SmartThings Keys](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md#smartthings-keys))\n- Shows TV channel names\n- Shows accurate states for HDMI or TV input sources\n\n\n***SmartThings Keys***\n---------------\n\n*Input Keys*\n____________\nKey|Description\n---|-----------\nST_TV|TV\nST_VD:`src`|`src`\nST_HDMI1|HDMI1\nST_HDMI2|HDMI2\nST_HDMI3|HDMI3\nST_HDMI4|HDMI4\n...\n\n\nWith ST_VD:`src` replace `src` with the name of the source you want activate\n\n\n*Channel Keys*\n______________\nKey|Description\n---|-----------\nST_CHUP|ChannelUp\nST_CHDOWN|ChannelDown\nST_CH1|Channel1\nST_CH2|Channel2\nST_CH3|Channel3\n...\n\n*Volume Keys*\n______________\nKey|Description\n---|-----------\nST_MUTE|Mute/Unmute\nST_VOLUP|VolumeUp\nST_VOLDOWN|VolumeDown\nST_VOL1|VolumeLevel1\nST_VOL2|VolumeLevel2\n...\nST_VOL100|VolumeLevel100\n"
  },
  {
    "path": "hacs.json",
    "content": "{\n  \"name\": \"SamsungTV Smart\",\n  \"content_in_root\": false,\n  \"zip_release\": true,\n  \"filename\": \"samsungtv_smart.zip\",\n  \"homeassistant\": \"2025.6.0\"\n}\n"
  },
  {
    "path": "info.md",
    "content": "\n# HomeAssistant - SamsungTV Smart Component\n\nThis is a custom component to allow control of SamsungTV devices in [HomeAssistant](https://home-assistant.io). \nIs a modified version of the built-in [samsungtv](https://www.home-assistant.io/integrations/samsungtv/) with some extra\n features.<br/>\n**This plugin is only for 2016+ TVs model!** (maybe all tizen family)\n\nThis project is a fork of the component [SamsungTV Tizen](https://github.com/jaruba/ha-samsungtv-tizen). I added some \nfeature like the possibility to configure it using the HA user interface, simplifing the configuration process. \nI also added some code optimizition in the comunication layer using async aiohttp instead of request.\n**Part of the code and documentation available here come from the original project.**<br/>\n\n# Additional Features:\n\n* Ability to send keys using a native Home Assistant service\n* Ability to send chained key commands using a native Home Assistant service\n* Supports Assistant commands (Google Home, should work with Alexa too, but untested)\n* Extended volume control\n* Ability to customize source list at media player dropdown list\n* Cast video URLs to Samsung TV\n* Connect to SmartThings Cloud API for additional features: see TV channel names, see which HDMI source is selected, more key codes to change input source\n* Display logos of TV channels (requires Smartthings enabled) and apps\n\n# Configuration\n\nOnce the component has been installed, you need to configure it using web UI in order to make it work.\n\n**Important**: To complete the configuration procedure properly, you must be sure that your **TV is turned on and \nconnected to the LAN (wired or wireless)**. Stay near to your TV during configuration because probably you will need \nto accept the access request that will prompt on your TV screen.\n\n**Note**: To configure the component for using **SmartThings (strongly suggested)** you need to generate an access \ntoken as explained in [this guide](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md). \nAlso make sure your **TV is logged into your SmartThings account** and **registered in SmartThings phone app** before \nstarting configuration.\n\n### Configuration using the web UI\n\n1. From the Home Assistant front-end, navigate to 'Configuration' then 'Integrations'. Click `+` button in botton right corner,\nsearch '**SamsungTV Smart**' and click 'Configure'.\n2. In the configuration mask, enter the IP address of the TV, the name for the Entity and the SmartThings personal \naccess token (if created) and then click 'Submit'\n3. **Important**: look for your TV screen and confirm **immediatly** with OK if a popup appear.\n4. Congrats! You're all set!\n\n**Note**: be sure that your TV and HA are connected to the same VLAN. Websocket connection through different VLAN normally\nnot work because not supported by Samsung TV.\nIf you have errors during configuration, try to power cycle your TV. This will close running applications that can prevent \nwebsocket connection initialization.\n\n**Please refer to [readme](https://github.com/ollo69/ha-samsungtv-smart/blob/master/README.md) for details on optional parameter and additional configuration instruction.**\n\n# Be kind!\nIf you like the component, why don't you support me by buying me a coffe?\nIt would certainly motivate me to further improve this work.\n\n[![Buy me a coffe!](https://www.buymeacoffee.com/assets/img/custom_images/black_img.png)](https://www.buymeacoffee.com/ollo69)\n\nCredits\n-------\n\nThis integration is developed by [Ollo69][ollo69] based on integration [SamsungTV Tizen][samsungtv_tizen].<br/>\nOriginal SamsungTV Tizen integration was developed by [jaruba][jaruba].<br/>\nLogo support is based on [jaruba channels-logo][channels-logo] and was developed with the support of [Screwdgeh][Screwdgeh].<br/>\n\n[ollo69]: https://github.com/ollo69\n[samsungtv_tizen]: https://github.com/jaruba/ha-samsungtv-tizen\n[jaruba]: https://github.com/jaruba\n[Screwdgeh]: https://github.com/Screwdgeh\n[channels-logo]: https://github.com/jaruba/channel-logos\n"
  },
  {
    "path": "requirements.txt",
    "content": "# Home Assistant Core\ncolorlog==6.8.2\nhomeassistant==2025.6.3\npip>=21.3.1,<=24.3.2\nruff==0.0.261\npre-commit==3.0.0\nflake8==6.1.0\nisort==5.12.0\nblack==25.1.0\nwebsocket-client!=1.4.0,>=0.58.0\nwakeonlan>=2.0.0\naiofiles>=0.8.0\ncasttube>=0.2.1\n"
  },
  {
    "path": "requirements_test.txt",
    "content": "# Strictly for tests\npytest==8.3.5\n#pytest-cov==2.9.0\n#pytest-homeassistant\npytest-homeassistant-custom-component==0.13.254\n# From our manifest.json for our custom component\nwebsocket-client!=1.4.0,>=0.58.0\nwakeonlan>=2.0.0\naiofiles>=0.8.0\ncasttube>=0.2.1\n"
  },
  {
    "path": "script/integration_init",
    "content": "#!/usr/bin/env bash\n\n# Create empty init in custom components directory\necho \"Init custom components directory\"\ntouch \"${PWD}/custom_components/__init__.py\"\n"
  },
  {
    "path": "scripts/develop",
    "content": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\n# Create config dir if not present\nif [[ ! -d \"${PWD}/config\" ]]; then\n    mkdir -p \"${PWD}/config\"\n    hass --config \"${PWD}/config\" --script ensure_config\nfi\n\n# Set the path to custom_components\n## This let's us have the structure we want <root>/custom_components/integration_blueprint\n## while at the same time have Home Assistant configuration inside <root>/config\n## without resulting to symlinks.\nexport PYTHONPATH=\"${PYTHONPATH}:${PWD}/custom_components\"\n\n# Start Home Assistant\n#hass --config \"${PWD}/config\" --debug\nhass --config \"${PWD}/config\"\n"
  },
  {
    "path": "scripts/lint",
    "content": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\nruff check . --fix\n"
  },
  {
    "path": "scripts/setup",
    "content": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\npython3 -m pip install --requirement requirements.txt\n"
  },
  {
    "path": "setup.cfg",
    "content": "[coverage:run]\nsource =\n  custom_components\n\n[coverage:report]\nexclude_lines =\n    pragma: no cover\n    raise NotImplemented()\n    if __name__ == '__main__':\n    main()\nshow_missing = true\n\n[tool:pytest]\ntestpaths = tests\nnorecursedirs = .git\naddopts =\n    --strict-markers\n    --cov=custom_components\n# asyncio_mode = auto\n\n[isort]\n# https://github.com/timothycrosley/isort\n# https://github.com/timothycrosley/isort/wiki/isort-Settings\n# splits long import on multiple lines indented by 4 spaces\nprofile = black\nline_length = 88\n# will group `import x` and `from x import` of the same module.\nforce_sort_within_sections = true\nsections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER\ndefault_section = THIRDPARTY\nknown_first_party = homeassistant\nknown_local_folder = custom_components, tests\nforced_separate = tests\ncombine_as_imports = true\n\n[flake8]\nexclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build\nmax-complexity = 25\ndoctests = True\n# To work with Black\n# E501: line too long\n# W503: Line break occurred before a binary operator\n# E203: Whitespace before ':'\n# D202 No blank lines allowed after function docstring\n# W504 line break after binary operator\nignore =\n    E501,\n    W503,\n    E203,\n    D202,\n    W504\nnoqa-require-code = True\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "\"\"\"custom integation tests.\"\"\"\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "\"\"\"Global fixtures for integration_blueprint integration.\"\"\"\n\n# Fixtures allow you to replace functions with a Mock object. You can perform\n# many options via the Mock to reflect a particular behavior from the original\n# function that you want to see without going through the function's actual logic.\n# Fixtures can either be passed into tests as parameters, or if autouse=True, they\n# will automatically be used across all tests.\n#\n# Fixtures that are defined in conftest.py are available across all tests. You can also\n# define fixtures within a particular test file to scope them locally.\n#\n# pytest_homeassistant_custom_component provides some fixtures that are provided by\n# Home Assistant core. You can find those fixture definitions here:\n# https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/blob/master/pytest_homeassistant_custom_component/common.py\n#\n# See here for more info: https://docs.pytest.org/en/latest/fixture.html (note that\n# pytest includes fixtures OOB which you can use as defined on this page)\nfrom unittest.mock import patch\n\nimport pytest\n\npytest_plugins = \"pytest_homeassistant_custom_component\"\n\n\n# This fixture enables loading custom integrations in all tests.\n# Remove to enable selective use of this fixture\n@pytest.fixture(autouse=True)\ndef auto_enable_custom_integrations(enable_custom_integrations):\n    yield\n\n\n# This fixture is used to prevent HomeAssistant from attempting to create and dismiss persistent\n# notifications. These calls would fail without this fixture since the persistent_notification\n# integration is never loaded during a test.\n@pytest.fixture(name=\"skip_notifications\", autouse=True)\ndef skip_notifications_fixture():\n    \"\"\"Skip notification calls.\"\"\"\n    with patch(\"homeassistant.components.persistent_notification.async_create\"), patch(\n        \"homeassistant.components.persistent_notification.async_dismiss\"\n    ):\n        yield\n"
  }
]