Full Code of ollo69/ha-samsungtv-smart for AI

master 1336a3557898 cached
59 files
381.6 KB
113.6k tokens
358 symbols
1 requests
Download .txt
Showing preview only (402K chars total). Download the full file or copy to clipboard to get everything.
Repository: ollo69/ha-samsungtv-smart
Branch: master
Commit: 1336a3557898
Files: 59
Total size: 381.6 KB

Directory structure:
gitextract_jb3sp0ji/

├── .devcontainer/
│   └── devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── hassfest.yaml
│       ├── linting.yaml
│       ├── release.yml
│       ├── stale.yaml
│       └── validate.yaml
├── .gitignore
├── .prettierignore
├── .pylintrc
├── .ruff.toml
├── .vscode/
│   ├── extensions.json
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── Dockerfile.dev
├── LICENSE
├── README.md
├── config/
│   └── configuration.yaml
├── custom_components/
│   ├── __init__.py
│   └── samsungtv_smart/
│       ├── __init__.py
│       ├── api/
│       │   ├── __init__.py
│       │   ├── samsungcast.py
│       │   ├── samsungws.py
│       │   ├── shortcuts.py
│       │   ├── smartthings.py
│       │   └── upnp.py
│       ├── config_flow.py
│       ├── const.py
│       ├── diagnostics.py
│       ├── entity.py
│       ├── logo.py
│       ├── logo_paths.json
│       ├── manifest.json
│       ├── media_player.py
│       ├── remote.py
│       ├── services.yaml
│       └── translations/
│           ├── en.json
│           ├── hu.json
│           ├── it.json
│           └── pt-BR.json
├── docs/
│   ├── App_list.md
│   ├── Key_chaining.md
│   ├── Key_codes.md
│   └── Smartthings.md
├── hacs.json
├── info.md
├── requirements.txt
├── requirements_test.txt
├── script/
│   └── integration_init
├── scripts/
│   ├── develop
│   ├── lint
│   └── setup
├── setup.cfg
└── tests/
    ├── __init__.py
    └── conftest.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .devcontainer/devcontainer.json
================================================
{
  "name": "SamsungTV Smart Integration",
  "dockerFile": "../Dockerfile.dev",
  "postCreateCommand": "scripts/setup",
  "forwardPorts": [8123],
  "portsAttributes": {
    "8123": {
      "label": "Home Assistant",
      "onAutoForward": "notify"
    }
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.black-formatter",
        "ms-python.pylint",
        "ms-python.vscode-pylance",
        "visualstudioexptteam.vscodeintellicode",
        "redhat.vscode-yaml",
        "esbenp.prettier-vscode",
        "GitHub.vscode-pull-request-github",
        "ryanluker.vscode-coverage-gutters"
      ],
      "settings": {
        "files.eol": "\n",
        "editor.tabSize": 4,
        "python.pythonPath": "/usr/local/bin/python",
        "python.testing.pytestArgs": ["--no-cov"],
        "python.analysis.autoSearchPaths": false,
        "editor.formatOnPaste": false,
        "editor.formatOnSave": true,
        "editor.formatOnType": true,
        "files.trimTrailingWhitespace": true,
        "terminal.integrated.profiles.linux": {
          "zsh": {
            "path": "/usr/bin/zsh"
          }
        },
        "terminal.integrated.defaultProfile.linux": "zsh",
        "[python]": {
          "editor.defaultFormatter": "ms-python.black-formatter"
        }
      }
    }
  },
  "remoteUser": "vscode"
}

================================================
FILE: .dockerignore
================================================
# General files
.git
.github
config
docs

# Development
.devcontainer
.vscode

# Test related files
tests

# Other virtualization methods
venv
.vagrant

# Temporary files
**/__pycache__

================================================
FILE: .gitattributes
================================================
# Ensure Docker script files uses LF to support Docker for Windows.
# Ensure "git config --global core.autocrlf input" before you clone
*     text eol=lf
*.py  whitespace=error

*.ico binary
*.gif binary
*.jpg binary
*.png binary
*.zip binary
*.mp3 binary

Dockerfile.dev linguist-language=Dockerfile


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**Expected behavior**
If applicable, a clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Environment details:**
 - Environment (HASSIO, Raspbian, etc):
 - Home Assistant version installed:
 - Component version installed:
 - Last know working version:
 - TV model:

**Output of HA logs**
Paste the relavant output of the HA log here.

```

```

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: Feature Request
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/workflows/hassfest.yaml
================================================
name: Validate with Hassfest

on:
  push:
    branches:
      - master

  pull_request:
    branches: ["*"]

  schedule:
    - cron: "0 0 * * *"

jobs:
  validate_hassfest:
    runs-on: "ubuntu-latest"
    steps:
      - uses: "actions/checkout@v5"
      - uses: home-assistant/actions/hassfest@master


================================================
FILE: .github/workflows/linting.yaml
================================================
name: Linting

on:
  push:
    branches:
      - master

  pull_request:
    branches: ["*"]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - name: Setup Python
        uses: actions/setup-python@v6
        with:
          python-version: 3.13

      - name: Install dependencies
        run: |
          pip install -r requirements.txt
      - name: flake8
        run: flake8 .
      - name: isort
        run: isort --diff --check .
      - name: Black
        run: black --line-length 88 --diff --check .


================================================
FILE: .github/workflows/release.yml
================================================
name: "Release"

on:
  release:
    types:
      - "published"

permissions: {}

jobs:
  release:
    name: "Release"
    runs-on: "ubuntu-latest"
    permissions:
      contents: write
    steps:
      - name: "Checkout the repository"
        uses: "actions/checkout@v5"

      - name: "ZIP the integration directory"
        shell: "bash"
        run: |
          cd "${{ github.workspace }}/custom_components/samsungtv_smart"
          zip samsungtv_smart.zip -r ./

      - name: "Upload the ZIP file to the release"
        uses: "softprops/action-gh-release@v2.0.8"
        with:
          files: ${{ github.workspace }}/custom_components/samsungtv_smart/samsungtv_smart.zip


================================================
FILE: .github/workflows/stale.yaml
================================================
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/actions/stale
name: "Close stale issues and PRs"

on:
  schedule:
  - cron: "0 2 * * *"
  workflow_dispatch:

permissions:
  contents: read

jobs:
  stale:
    permissions:
      issues: write  # for actions/stale to close stale issues
      pull-requests: write  # for actions/stale to close stale PRs
    runs-on: ubuntu-latest
    steps:
    - uses: actions/stale@v10
      with:
        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.'
        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.'
        close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
        close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.'
        exempt-issue-labels: 'Feature Request,documentation'
        days-before-issue-stale: 45
        days-before-pr-stale: -1
        days-before-issue-close: 7
        days-before-pr-close: -1
        ascending: true
        operations-per-run: 400


================================================
FILE: .github/workflows/validate.yaml
================================================
name: Validate with Hacs

on:
  push:
    branches:
      - master

  pull_request:
    branches: ["*"]

  schedule:
    - cron: "0 0 * * *"

jobs:
  validate_hacs:
    runs-on: "ubuntu-latest"
    steps:
      - uses: "actions/checkout@v5"
      - name: HACS validation
        uses: "hacs/action@main"
        with:
          category: "integration"


================================================
FILE: .gitignore
================================================
# artifacts
__pycache__
.pytest*
.cache
*.egg-info
*/build/*
*/dist/*

# pycharm
.idea/

# Unit test / coverage reports
.coverage
coverage.xml

# Home Assistant configuration
config/*
!config/configuration.yaml

================================================
FILE: .prettierignore
================================================
*.md
.strict-typing
azure-*.yml
docs/source/_templates/*


================================================
FILE: .pylintrc
================================================
[MESSAGES CONTROL]
# PyLint message control settings
# Reasons disabled:
# format - handled by black
# locally-disabled - it spams too much
# duplicate-code - unavoidable
# cyclic-import - doesn't test if both import on load
# abstract-class-little-used - prevents from setting right foundation
# unused-argument - generic callbacks and setup methods create a lot of warnings
# too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-*
# abstract-method - with intro of async there are always methods missing
# inconsistent-return-statements - doesn't handle raise
# too-many-ancestors - it's too strict.
# wrong-import-order - isort guards this
# consider-using-f-string - str.format sometimes more readable
# ---
# Enable once current issues are fixed:
# consider-using-namedtuple-or-dataclass (Pylint CodeStyle extension)
# consider-using-assignment-expr (Pylint CodeStyle extension)
disable =
    format,
    abstract-method,
    cyclic-import,
    duplicate-code,
    inconsistent-return-statements,
    locally-disabled,
    not-context-manager,
    too-few-public-methods,
    too-many-ancestors,
    too-many-arguments,
    too-many-branches,
    too-many-instance-attributes,
    too-many-lines,
    too-many-locals,
    too-many-public-methods,
    too-many-return-statements,
    too-many-statements,
    too-many-boolean-expressions,
    unused-argument,
    wrong-import-order,
    consider-using-f-string,
    unexpected-keyword-arg
#    consider-using-namedtuple-or-dataclass,
#    consider-using-assignment-expr


================================================
FILE: .ruff.toml
================================================
# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml

target-version = "py310"

select = [
    "B007", # Loop control variable {name} not used within loop body
    "B014", # Exception handler with duplicate exception
    "C",  # complexity
    "D",  # docstrings
    "E",  # pycodestyle
    "F",  # pyflakes/autoflake
    "ICN001", # import concentions; {name} should be imported as {asname}
    "PGH004",  # Use specific rule codes when using noqa
    "PLC0414", # Useless import alias. Import alias does not rename original package.
    "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass
    "SIM117", # Merge with-statements that use the same scope
    "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys()
    "SIM201", # Use {left} != {right} instead of not {left} == {right}
    "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a}
    "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'.
    "SIM401", # Use get from dict with default instead of an if block
    "T20",  # flake8-print
    "TRY004", # Prefer TypeError exception for invalid type
    "RUF006", # Store a reference to the return value of asyncio.create_task
    "UP",  # pyupgrade
    "W",  # pycodestyle
]

ignore = [
    "D202",  # No blank lines allowed after function docstring
    "D203",  # 1 blank line required before class docstring
    "D213",  # Multi-line docstring summary should start at the second line
    "D404",  # First word of the docstring should not be This
    "D406",  # Section name should end with a newline
    "D407",  # Section name underlining
    "D411",  # Missing blank line before section
    "E501",  # line too long
    "E731",  # do not assign a lambda expression, use a def
]

[flake8-pytest-style]
fixture-parentheses = false

[pyupgrade]
keep-runtime-typing = true

[mccabe]
max-complexity = 25

================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": ["esbenp.prettier-vscode", "ms-python.python"]
}


================================================
FILE: .vscode/launch.json
================================================
{
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            // Example of attaching to local debug server
            "name": "Python: Attach Local",
            "type": "python",
            "request": "attach",
            "port": 5678,
            "host": "localhost",
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}",
                    "remoteRoot": "."
                }
            ],
        },
        {
            // Example of attaching to my production server
            "name": "Python: Attach Remote",
            "type": "python",
            "request": "attach",
            "port": 5678,
            "host": "homeassistant.local",
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}",
                    "remoteRoot": "/usr/src/homeassistant"
                }
            ],
        }
    ]
}


================================================
FILE: .vscode/settings.json
================================================
{
  //"editor.formatOnSave": true
  "[python]": {
    "editor.defaultFormatter": "ms-python.black-formatter",
    "editor.formatOnSave": true
  },
  // Added --no-cov to work around TypeError: message must be set
  // https://github.com/microsoft/vscode-python/issues/14067
  "python.testing.pytestArgs": ["--no-cov"],
  // https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
  "python.testing.pytestEnabled": false
}


================================================
FILE: .vscode/tasks.json
================================================
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Run Home Assistant on port 8123",
            "type": "shell",
            "command": "scripts/develop",
            "problemMatcher": []
        },
		{
		  "label": "Install Requirements",
		  "type": "shell",
		  "command": "pip3 install --use-deprecated=legacy-resolver -r requirements.txt",
		  "group": {
			"kind": "build",
			"isDefault": true
		  },
		  "presentation": {
			"reveal": "always",
			"panel": "new"
		  },
		  "problemMatcher": []
		},
		{
		  "label": "Install Test Requirements",
		  "type": "shell",
		  "command": "pip3 install --use-deprecated=legacy-resolver -r requirements_test.txt",
		  "group": {
			"kind": "build",
			"isDefault": true
		  },
		  "presentation": {
			"reveal": "always",
			"panel": "new"
		  },
		  "problemMatcher": []
		},
		{
		  "label": "Run PyTest",
		  "detail": "Run pytest for integration.",
		  "type": "shell",
		  "command": "pytest --cov-report term-missing -vv --durations=10",
		  "group": {
			"kind": "test",
			"isDefault": true
		  },
		  "presentation": {
			"reveal": "always",
			"panel": "new"
		  },
		  "problemMatcher": []
		}
    ]
}


================================================
FILE: Dockerfile.dev
================================================
FROM mcr.microsoft.com/devcontainers/python:1-3.13

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Uninstall pre-installed formatting and linting tools
# They would conflict with our pinned versions
RUN \
    pipx uninstall pydocstyle \
    && pipx uninstall pycodestyle \
    && pipx uninstall mypy \
    && pipx uninstall pylint

RUN \
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && apt-get update \
    && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        # Additional library needed by some tests and accordingly by VScode Tests Discovery
        bluez \
        ffmpeg \
        libudev-dev \
        libavformat-dev \
        libavcodec-dev \
        libavdevice-dev \
        libavutil-dev \
        libgammu-dev \
        libswscale-dev \
        libswresample-dev \
        libavfilter-dev \
        libpcap-dev \
        libturbojpeg0 \
        libyaml-dev \
        libxml2 \
        git \
        cmake \
        autoconf \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# Add go2rtc binary
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc

# Install uv
RUN pip3 install uv

WORKDIR /workspaces

# Set the default shell to bash instead of sh
ENV SHELL /bin/bash


================================================
FILE: LICENSE
================================================
Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
[![](https://img.shields.io/github/release/ollo69/ha-samsungtv-smart/all.svg?style=for-the-badge)](https://github.com/ollo69/ha-samsungtv-smart/releases)
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs)
[![](https://img.shields.io/github/license/ollo69/ha-samsungtv-smart?style=for-the-badge)](LICENSE)
[![](https://img.shields.io/badge/MAINTAINER-%40ollo69-red?style=for-the-badge)](https://github.com/ollo69)
[![](https://img.shields.io/badge/COMMUNITY-FORUM-success?style=for-the-badge)](https://community.home-assistant.io)

# HomeAssistant - SamsungTV Smart Component

This is a custom component to allow control of SamsungTV devices in [HomeAssistant](https://home-assistant.io).
Is a modified version of the built-in [samsungtv](https://www.home-assistant.io/integrations/samsungtv/) with some extra
 features.<br/>
**This plugin is only for 2016+ TVs model!** (maybe all tizen family)

This project is a fork of the component [SamsungTV Tizen](https://github.com/jaruba/ha-samsungtv-tizen). I added some
feature like the possibility to configure it using the HA user interface, simplifing the configuration process.
I also added some code optimizition in the comunication layer using async aiohttp instead of request.
**Part of the code and documentation available here come from the original project.**<br/>

# Additional Features:

* Ability to send keys using a native Home Assistant service
* Ability to send chained key commands using a native Home Assistant service
* Supports Assistant commands (Google Home, should work with Alexa too, but untested)
* Extended volume control
* Ability to customize source list at media player dropdown list
* Cast video URLs to Samsung TV
* 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
* Display logos of TV channels (requires Smartthings enabled) and apps

![N|Solid](https://i.imgur.com/8mCGZoO.png)
![N|Solid](https://i.imgur.com/t3e4bJB.png)

# Installation

### 1. Easy Mode

Install via HACS.

### 2. Manual

Install it as you would do with any homeassistant custom component:

1. Download `custom_components` folder.
1. Copy the `samsungtv_smart` directory within the `custom_components` directory of your homeassistant installation. The `custom_components` directory resides within your homeassistant configuration directory.
**Note**: if the custom_components directory does not exist, you need to create it.
After a correct installation, your configuration directory should look like the following.
    ```
    └── ...
    └── configuration.yaml
    └── custom_components
        └── samsungtv_smart
            └── __init__.py
            └── media_player.py
            └── websockets.py
            └── shortcuts.py
            └── smartthings.py
            └── upnp.py
            └── exceptions.py
            └── ...
    ```

# Configuration

Once the component has been installed, you need to configure it in order to make it work.
There are two ways of doing so:
- Using the web interface (Lovelace) [**recommended**]
- Manually editing the `configuration.yaml` file

**Important**: To complete the configuration procedure properly, you must be sure that your **TV is turned on and
connected to the LAN (wired or wireless)**. Stay near to your TV during configuration because probably you will need
to accept the access request that will prompt on your TV screen.

**Note**: To configure the component for using **SmartThings (strongly suggested)** you need to generate an access
token as explained in [this guide](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md).
Also make sure your **TV is logged into your SmartThings account** and **registered in SmartThings phone app** before
starting configuration.

### Option A: Configuration using the web UI [**recommended**]

1. From the Home Assistant front-end, navigate to 'Configuration' then 'Integrations'. Click `+` button in botton right corner,
search '**SamsungTV Smart**' and click 'Configure'.
2. In the configuration mask, enter the IP address of the TV, the name for the Entity and the SmartThings personal
access token (if created) and then click 'Submit'
3. **Important**: look for your TV screen and confirm **immediately** with OK if a popup appear.
4. Congrats! You're all set!

**Note**: be sure that your TV and HA are connected to the same VLAN. Websocket connection through different VLAN normally
not work because not supported by Samsung TV.
If you have errors during configuration, try to power cycle your TV. This will close running applications that can prevent
websocket connection initialization.

### Option B: Configuration via editing `configuration.yaml`

**From v0.3.16 initial configuration from yaml is not allowed.**<br>
You can still use `configuration.yaml` to set the additional parameter as explained below.

## Configuration options

From the Home Assistant front-end, navigate to 'Configuration' then 'Integrations'. Identify the '**SamsungTV Smart**'
integration configured for your TV and click the `OPTIONS` button.<br/>
Here you can change the following options:

- **Use SmartThings TV Status information**<br/>
(default = True)<br/>
**This option is available only if SmartThings is configured.**
When enabled the component will try to retrieve from SmartThings the information
about the TV Status. This information is always used in conjunction with local ping result.<br/>

- **Use SmartThings TV Channels information**<br/>
(default = True)<br/>
**This option is available only if SmartThings is configured.**
When enabled the component will try to retrieve from SmartThings the information about the TV Channel
and TV Channel Name or the Running App<br/>
**Note: in some case this information is not properly updated, disabled it you have incorrect information.**<br/>

- **Use SmartThings TV Channels number information**<br/>
(default = False)<br/>
**This option is available only if SmartThings is configured.**
When enabled then the TV Channel Names will show as media titles, by setting this to True the
TV Channel Number will also be attached to the end of the media title (when applicable).<br/>
**Note: not always SmartThings provide the information for channel_name and channel_number.**<br/>

- **Logo options**<br/>
The background color and channel / service logo preference to use, example: "white-color" (background: white, logo: color).<br/>
Supported values: "none", "white-color", "dark-white", "blue-color", "blue-white", "darkblue-white", "transparent-color", "transparent-white"<br/>
Default value: "white-color" (background: white, logo: color)<br/>
Notice that your logo is missing or outdated? In case of a missing TV channel logo also make sure you have Smartthings enabled.
This is required for the component to know the name of the TV channel.<br/>
Check guide [here](https://github.com/jaruba/ha-samsungtv-tizen/blob/master/Logos.md)
for updating the logo database this component is relying on.

- **Allow use of local logo images**<br/>
(default = True)<br/>
When 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.
You can add new logo images in this folder, using the following rules for logo filename:
  - must be equal to the name of the `media_title` attribute, removing space, `_` and `.` characters and replacing `+` character with
  the string `plus`
  - must have the `.png` suffix
  - must be in `png` format (suggested size is 400x400 pixels)

- **Applications list load mode at startup**<br/>
Possible values: `All Apps`, `Default Apps` and `Not Load`<br/>
This option determine the mode application list is automatic generated.<br>
With `All Apps` the list will contain all apps installed on the TV, with `Default Apps` will be generated a minimal list
with only the most common application, with `Not Load` application list will be empty.<br/>
**Note: If a custom `Application List` in config options is defined this option is not available.**<br>

- **Method used to turn on TV**<br/>
Possible values: `WOL Packet` and `SmartThings`<br/>
**This option is available only if SmartThings is configured.**
WOL Packet is better when TV use wired connection.<br/>
SmartThings normally work only when TV use wireless connection.<br/>

- **Show advanced options**<br/>
Selecting this option and clicking submit a new options menu is opened containing the list of other options described below.

### Advanced options

- **Applications launch method used**<br/>
Possible values: `Control Web Socket Channel`, `Remote Web Socket Channel` and `Rest API Call`<br/>
This option determine the mode used to launch applications.<br/>
Use `Rest API Call` only if the other 2 methods do not work.<br/>

- **Number of times WOL packet is sent to turn on TV**<br/>
(default = 1, range from 1 to 5)<br/>
This option allow to configure the number of time the WOL packet is sent to turn on TV. Increase the value
until the TV properly turn-on.<br/>

- **TCP port used to check power status**<br/>
(default = 0, range from 0 to 65535)<br/>
With this option is possible to check the availability of a specific port to determinate power status instead
of using ICMP echo. To continue use ICMP echo, leave the value to `0`, otherwise set a port that is known becoming
available when TV is on (possible working ports, depending on TV models, are `9110`, `9119`, `9197`).</br>
**N.B. If you set an invalid port here, TV is always reported as `off`.**</br>

- **Binary sensor to help detect power status**<br/>
An external `binary_sensor` selectable from a list that can be used to determinate TV power status.<br/>
This can be any available `binary_sensor` that can better determinate the status of the TV, for example a
`binary_sensor` based on TV power consumption. It is suggested to not use a sensor based on `ping` platform
because this method is already implemented by the integration.</br>

- **Use volume mute status to detect fake power ON**<br/>
(default = True)<br/>
When enabled try to detect fake power on based on the Volume mute state, based on the assumption that when the
TV is powered on the volume is always unmuted.<br/>

- **Dump apps list on log file at startup**<br/>
(default = False)<br/>
When enabled the component will try to dump the list of available apps on TV in the HA log file at Info level.
The dump of the apps may not work for some TV models.<br/>

- **Power button switch to art mode**<br/>
(default = False)<br/>
When enabled the power button in UI will be used to toggle from `On` to `Art Mode` (and vice versa) and will not
power off the TV (you can still use the `turn off` service to power off the TV).<br/>
**Note: This option is valid only for TV that support `Art Mode` ("The Frame" models).**<br>

### Synched entities configuration

- **List of entity to Power OFF with TV**<br/>
A list of HA entity to Turn OFF when the TV entity is turned OFF (maximum 4). Select entities from list.
This call the service `homeassistant.turn_off` for maximum the first 4 entity in the provided list.<br/>

- **List of entity to Power ON with TV**<br/>
A list of HA entity to Turn ON when the TV entity is turned ON (maximum 4).  Select entities from list.
This call the service `homeassistant.turn_on` for maximum the first 4 entity in the provided list.<br/>

### Sources list configuration

This contains the KEYS visible sources in the dropdown list in media player UI.<br/>
You can configure the pair list `Name: Key` using the yaml editor in the option page. If a source list is present in
`configuration.yaml`, it will be imported in the options the first time that the integration is loaded.<br/>

Default value:<br/>
```
    1| TV: KEY_TV
    2| HDMI: KEY_HDMI
```

If SmartThings is [configured](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md) and the
source_list not, the component will try to identify and configure automatically the sources configured on the TV with
the relative associated names (new feature, tested on QLed TV). The created list is available in the HA log file.<br/>
You can also chain KEYS, example:
```
    1| TV: KEY_SOURCES+KEY_ENTER
```

And even add delays (in milliseconds) between sending KEYS, example:<br/>
```
    1| TV: KEY_SOURCES+500+KEY_ENTER
```

Resources: [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/>
**Warning: changing input source with voice commands only works if you set the device name in `source_list` as one of
the 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)
(under "Mode Settings")**<br/>

### Application list configuration

This contains the APPS visible sources in the dropdown list in media player UI.<br/>
You can configure the pair list `Name: Key` using the yaml editor in the option page. If an application list is present in
`configuration.yaml`, it will be imported in the options the first time that the integration is loaded.<br/>

If the `Application list` is not manually configured, during startup the integration will try to automatically generate a list
of available application and a log message is generated with the content of the list. 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). Automatic list
generation not work with some TV models.<br/>

Example value:
```
    1| Netflix: "11101200001"
    2| YouTube: "111299001912"
    3| Spotify: "3201606009684"
```

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)<br/>

### Channel list configuration

This contains the tv CHANNELS visible sources in the dropdown list in media player UI. To guarantee performance keep the list small,
recommended maximum 30 channels.<br/>
You can configure the pair list `Name: Key` using the yaml editor in the option page. If a channel list is present in
`configuration.yaml`, it will be imported in the options the first time that the integration is loaded.<br/>

Example value:
```
    1| MTV: "14"
    2| Eurosport: "20"
    3| TLC: "21"
```

You 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/>
Example value:
```
    1| MTV: 14@TV
    2| Eurosport: 20@TV
    3| TLC: 21@HDMI
```

## Custom configuration parameters

You can configure additional option for the component using configuration variable in `configuration.yaml` section.<br/>

Section in `configuration.yaml` file can also not be present and is not required for component to work. If you
want to configure any parameters, you must create one section that start with `- host` as shown in the example below:<br/>
```
samsungtv_smart:
  - host: <YOUR TV IP ADDRES>
    ...
```
Then you can add any of the following parameters:<br/>

- **mac:**<br/>
(string)(Optional)<br/>
This is an optional value, normally is automatically detected during setup phase and so is not required to specify it.
You should try to configure this parameter only if the setup fail in the detection.<br/>
The 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
from your network router.<br/>

- **broadcast_address:**<br/>
(string)(Optional)<br/>
**Do not set this option if you do not know what it does, it can break turning your TV on.**<br/>
The ip address of the host to send the magic packet (for wakeonlan) to if the "mac" property is also set.<br/>
Default value: "255.255.255.255"<br/>
Example value: "192.168.1.255"<br/>

### Deprecated configuration parameters

Deprecated parameters were used by old integration version. Are still valid but normally are automatically imported
in application options and not used anymore, so after first import can be removed from `configuration.yaml`.

- **source_list:**<br/>
(json)(Optional)<br/>
This contains the KEYS visible sources in the dropdown list in media player UI.<br/>
Default value: '{"TV": "KEY_TV", "HDMI": "KEY_HDMI"}'<br/>
If SmartThings is [configured](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md) and the
source_list not, the component will try to identify and configure automatically the sources configured on the TV with
the relative associated names (new feature, tested on QLed TV). The created list is available in the HA log file.<br/>
You can also chain KEYS, example: '{"TV": "KEY_SOURCES+KEY_ENTER"}'<br/>
And even add delays (in milliseconds) between sending KEYS, example:<br/>
    '{"TV": "KEY_SOURCES+500+KEY_ENTER"}'<br/>
Resources: [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/>
**Warning: changing input source with voice commands only works if you set the device name in `source_list` as one of
the 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)
(under "Mode Settings")**<br/>

- **app_list:**<br/>
(json)(Optional)<br/>
This contains the APPS visible sources in the dropdown list in media player UI.<br/>
Default value: AUTOGENERATED<br/>
If the `app_list` is not manually configured, during startup is generated a file in the custom component folder with the
list 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/>
Example value: '{"Netflix": "11101200001", "YouTube": "111299001912", "Spotify": "3201606009684"}'<br/>
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)<br/>

- **channel_list:**<br/>
(json)(Optional)<br/>
This contains the tv CHANNELS visible sources in the dropdown list in media player UI. To guarantee performance keep the list small,
recommended maximum 30 channels.<br/>
Example value: '{"MTV": "14", "Eurosport": "20", "TLC": "21"}'<br/>
You 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/>
Example value: '{"MTV": "14@TV", "Eurosport": "20@TV", "TLC": "21@HDMI"}'<br/>


### Removed configuration parameters

Removed parameters were used by old integration version, are not used and supported anymore and replaced by application option.
For this reason should be removed from `configuration.yaml`.

- **api_key:**<br/>
(string)(Optional) (obsolete/not used from v0.3.16 - configuration from yaml is not allowed)<br/>
API Key for the SmartThings Cloud API, this is optional but adds better state handling on, off, channel name, hdmi source,
and 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/>
Read [How to get an API Key for SmartThings](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md)<br/>
This parameter can also be provided during the component configuration using the user interface.<br/>
**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/>

- **device_id:**<br/>
(string)(Optional) (obsolete/not used from v0.3.16 - configuration from yaml is not allowed)<br/>
Device ID for the SmartThings Cloud API. This is optional, to be used only if component fails to automatically determinate it.
Read [SmartThings Device ID](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Smartthings.md#smartthings-device-id)
to understand how identify the correct value to use.<br/>
This parameter will be requested during component configuration from user interface when required.<br/>
**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/>

- **device_name:** (obsolete/not used from v0.3.16 - configuration from yaml is not allowed)<br/>
(string)(Optional)<br/>
This is an optional value, used only to identify the TV in SmartThings during initial configuration if you have more TV
registered. You should  configure this parameter only if the setup fails in the detection.<br/>
The device_name to use can be read using the SmartThings app<br/>
**Note: this parameter is used only during initial configuration.**<br/>

- **show_channel_number:** (obsolete/not used from v0.3.16 and replaced by Configuration options)<br/>
(boolean)(Optional)<br/>
If the SmartThings API is enabled (by settings "api_key" and "device_id"), then the TV Channel Names will show as media
titles, by setting this to True the TV Channel Number will also be attached to the end of the media title (when applicable).<br/>
**Note: not always SmartThings provide the information for channel_name and channel_number.**<br/>

- **load_all_apps:** (obsolete/not used from v0.3.4 and replaced by Configuration options)<br/>
(boolean)(Optional)<br/>
This option is `True` by default.</br>
Setting this parameter to false, if a custom `app_list` is not defined, the automatic app_list will be generated
limited to few application (the most common).<br/>

- **update_method:** (obsolete/not used from v0.3.3)<br/>
(string)(Optional)<br/>
This change the ping method used for state update. Values: "ping", "websockets" and "smartthings"<br/>
Default value: "ping" if SmartThings is not enabled, else "smartthings"<br/>
Example update_method: "websockets"<br/>

- **update_custom_ping_url:** (obsolete/not used from v0.2.x)<br/>
(string)(Optional)<br/>
Use custom endpoint to ping.<br/>
Default value: PING TO 8001 ENDPOINT<br/>
Example update_custom_ping_url: "http://192.168.1.77:9197/dmr"<br/>

- **scan_app_http:** (obsolete/not used from v0.2.x)<br/>
(boolean)(Optional)<br/>
This option is `True` by default. In some cases (if numerical IDs are used when setting `app_list`) HTTP polling will
be used (1 request per app) to get the running app.<br/>
This is a lengthy task that some may want to disable, you can do so by setting this option to `False`.<br/>
For 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/>

# Usage

### Known Supported Voice Commands

* Turn on `SAMSUNG-TV-NAME-HERE` (for some older TVs this only works if the TV is connected by LAN cable to the Network)
* Turn off `SAMSUNG-TV-NAME-HERE`
* Volume up on `SAMSUNG-TV-NAME-HERE` (increases volume by 1)
* Volume down on `SAMSUNG-TV-NAME-HERE` (decreases volume by 1)
* Set volume to 50 on `SAMSUNG-TV-NAME-HERE` (sets volume to 50 out of 100)
* Mute `SAMSUNG-TV-NAME-HERE` (sets volume to 0)
* 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")

(if you find more supported voice commands, please create an issue so I can add them here)

### Cast to TV

```
service: media_player.play_media
```

```json
{
  "entity_id": "media_player.samsungtv",
  "media_content_type": "url",
  "media_content_id": "FILE_URL",
}
```
Replace `FILE_URL` with the url of your file

### Cast to YouTube

```
service: media_player.play_media
```

```json
{
  "entity_id": "media_player.samsungtv",
  "media_content_type": "url",
  "media_content_id": "YOUTUBE_URL",
  "enqueue": "play",
}
```
Replace `YOUTUBE_URL` with the url of the video you want to play
All 4 enqueue modes are supported. Shorts videos URL are also supported.
**Note**: `enqueue` is required, or the service will open the video using TV Web Browser.

### Send Keys

```
service: media_player.play_media
```

```json
{
  "entity_id": "media_player.samsungtv",
  "media_content_type": "send_key",
  "media_content_id": "KEY_CODE"
}
```
**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))

Script example:
```
tv_channel_down:
  alias: Channel down
  sequence:
  - service: media_player.play_media
    data:
      entity_id: media_player.samsung_tv55
      media_content_type: "send_key"
      media_content_id: KEY_CHDOWN
```

### Hold Keys
```
service: media_player.play_media
```

```json
{
  "entity_id": "media_player.samsungtv",
  "media_content_type": "send_key",
  "media_content_id": "KEY_CODE, <hold_time>"
}
```

**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).

***Key Chaining Patterns***
---------------
Key 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.

[See the list of known Key Chaining Patterns](https://github.com/ollo69/ha-samsungtv-smart/blob/master/docs/Key_chaining.md)


***Open Browser Page***
---------------

```
service: media_player.play_media
```

```json
{
  "entity_id": "media_player.samsungtv",
  "media_content_type": "browser",
  "media_content_id": "https://www.google.com"
}
```

***Send Text***
---------------
To send a specific text to a selected text input

```
service: media_player.play_media
```

```json
{
  "entity_id": "media_player.samsungtv",
  "media_content_type": "send_text",
  "media_content_id": "your text"
}
```

***Select sound mode (SmartThings only)***
---------------

```
service: media_player.select_sound_mode
```

```json
{
  "entity_id": "media_player.samsungtv",
  "sound_mode": "your mode"
}
```

**Note**: You can get list of valid `sound_mode` in the `sound_mode_list` state attribute


***Select picture mode (SmartThings only)***
---------------

```
service: samsungtv_smart.select_picture_mode
```

```json
{
  "entity_id": "media_player.samsungtv",
  "picture_mode": "your mode"
}
```

**Note**: You can get list of valid `picture_mode` in the `picture_mode_list` state attribute


***Set Art Mode (for TV that support it)***
---------------

```
service: samsungtv_smart.set_art_mode
```

```json
{
  "entity_id": "media_player.samsungtv"
}
```

# Be kind!
If you like the component, why don't you support me by buying me a coffe?
It would certainly motivate me to further improve this work.

[![Buy me a coffe!](https://www.buymeacoffee.com/assets/img/custom_images/black_img.png)](https://www.buymeacoffee.com/ollo69)

Credits
-------

This integration is developed by [Ollo69][ollo69] based on integration [SamsungTV Tizen][samsungtv_tizen].<br/>
Original SamsungTV Tizen integration was developed by [jaruba][jaruba].<br/>
Logo support is based on [jaruba channels-logo][channels-logo] and was developed with the support of [Screwdgeh][Screwdgeh].<br/>

[ollo69]: https://github.com/ollo69
[samsungtv_tizen]: https://github.com/jaruba/ha-samsungtv-tizen
[jaruba]: https://github.com/jaruba
[Screwdgeh]: https://github.com/Screwdgeh
[channels-logo]: https://github.com/jaruba/channel-logos


================================================
FILE: config/configuration.yaml
================================================
# Loads default set of integrations. Do not remove.
default_config:

# Load frontend themes from the themes folder
frontend:
  themes: !include_dir_merge_named themes

# Text to speech
tts:
  - platform: google_translate

logger:
  default: info
  #logs:
  #  custom_components.samsungtv_smart: debug


================================================
FILE: custom_components/__init__.py
================================================
"""Custom components module."""


================================================
FILE: custom_components/samsungtv_smart/__init__.py
================================================
"""The samsungtv_smart integration."""

from __future__ import annotations

import asyncio
import json
import logging
import os
from pathlib import Path
import socket

from aiohttp import ClientConnectionError, ClientResponseError, ClientSession
import async_timeout
import voluptuous as vol
from websocket import WebSocketException

from homeassistant.components.http import StaticPathConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
    ATTR_DEVICE_ID,
    CONF_ACCESS_TOKEN,
    CONF_API_KEY,
    CONF_BROADCAST_ADDRESS,
    CONF_DEVICE_ID,
    CONF_HOST,
    CONF_ID,
    CONF_MAC,
    CONF_NAME,
    CONF_PORT,
    CONF_TIMEOUT,
    CONF_TOKEN,
    MAJOR_VERSION,
    MINOR_VERSION,
    Platform,
    __version__,
)
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType

from .api.samsungws import ConnectionFailure, SamsungTVWS
from .api.smartthings import SmartThingsTV
from .const import (
    ATTR_DEVICE_MAC,
    ATTR_DEVICE_MODEL,
    ATTR_DEVICE_NAME,
    ATTR_DEVICE_OS,
    CONF_APP_LIST,
    CONF_CHANNEL_LIST,
    CONF_DEVICE_NAME,
    CONF_LOAD_ALL_APPS,
    CONF_SCAN_APP_HTTP,
    CONF_SHOW_CHANNEL_NR,
    CONF_SOURCE_LIST,
    CONF_ST_ENTRY_UNIQUE_ID,
    CONF_SYNC_TURN_OFF,
    CONF_SYNC_TURN_ON,
    CONF_UPDATE_CUSTOM_PING_URL,
    CONF_UPDATE_METHOD,
    CONF_USE_ST_INT_API_KEY,
    CONF_WS_NAME,
    DATA_CFG,
    DATA_CFG_YAML,
    DATA_OPTIONS,
    DEFAULT_PORT,
    DEFAULT_SOURCE_LIST,
    DEFAULT_TIMEOUT,
    DOMAIN,
    LOCAL_LOGO_PATH,
    MIN_HA_MAJ_VER,
    MIN_HA_MIN_VER,
    RESULT_NOT_SUCCESSFUL,
    RESULT_ST_DEVICE_NOT_FOUND,
    RESULT_SUCCESS,
    RESULT_WRONG_APIKEY,
    SIGNAL_CONFIG_ENTITY,
    WS_PREFIX,
    __min_ha_version__,
)
from .logo import CUSTOM_IMAGE_BASE_URL, STATIC_IMAGE_BASE_URL

# workaroud for failing import native domain when custom integration is present
try:
    from homeassistant.components.smartthings.const import DOMAIN as ST_DOMAIN
except ImportError:
    ST_DOMAIN = "smartthings"

DEVICE_INFO = {
    ATTR_DEVICE_ID: "id",
    ATTR_DEVICE_MAC: "wifiMac",
    ATTR_DEVICE_NAME: "name",
    ATTR_DEVICE_MODEL: "modelName",
    ATTR_DEVICE_OS: "OS",
}

SAMSMART_PLATFORM = [Platform.MEDIA_PLAYER, Platform.REMOTE]

SAMSMART_SCHEMA = {
    vol.Optional(CONF_SOURCE_LIST, default=DEFAULT_SOURCE_LIST): cv.string,
    vol.Optional(CONF_APP_LIST): cv.string,
    vol.Optional(CONF_CHANNEL_LIST): cv.string,
    vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
    vol.Optional(CONF_MAC): cv.string,
    vol.Optional(CONF_BROADCAST_ADDRESS): cv.string,
}


def ensure_unique_hosts(value):
    """Validate that all configs have a unique host."""
    vol.Schema(vol.Unique("duplicate host entries found"))(
        [socket.gethostbyname(entry[CONF_HOST]) for entry in value]
    )
    return value


CONFIG_SCHEMA = vol.Schema(
    {
        DOMAIN: vol.All(
            cv.ensure_list,
            [
                cv.deprecated(CONF_LOAD_ALL_APPS),
                cv.deprecated(CONF_PORT),
                cv.deprecated(CONF_UPDATE_METHOD),
                cv.deprecated(CONF_UPDATE_CUSTOM_PING_URL),
                cv.deprecated(CONF_SCAN_APP_HTTP),
                vol.Schema(
                    {
                        vol.Required(CONF_HOST): cv.string,
                        vol.Optional(CONF_NAME): cv.string,
                        vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
                        vol.Optional(CONF_API_KEY): cv.string,
                        vol.Optional(CONF_DEVICE_NAME): cv.string,
                        vol.Optional(CONF_DEVICE_ID): cv.string,
                        vol.Optional(CONF_LOAD_ALL_APPS, default=True): cv.boolean,
                        vol.Optional(CONF_UPDATE_METHOD): cv.string,
                        vol.Optional(CONF_UPDATE_CUSTOM_PING_URL): cv.string,
                        vol.Optional(CONF_SCAN_APP_HTTP, default=True): cv.boolean,
                        vol.Optional(CONF_SHOW_CHANNEL_NR, default=False): cv.boolean,
                        vol.Optional(CONF_WS_NAME): cv.string,
                    }
                ).extend(SAMSMART_SCHEMA),
            ],
            ensure_unique_hosts,
        )
    },
    extra=vol.ALLOW_EXTRA,
)

_LOGGER = logging.getLogger(__name__)


def tv_url(host: str, address: str = "") -> str:
    """Return url to the TV."""
    return f"http://{host}:8001/api/v2/{address}"


def is_min_ha_version(min_ha_major_ver: int, min_ha_minor_ver: int) -> bool:
    """Check if HA version at least a specific version."""
    return MAJOR_VERSION > min_ha_major_ver or (
        MAJOR_VERSION == min_ha_major_ver and MINOR_VERSION >= min_ha_minor_ver
    )


def is_valid_ha_version() -> bool:
    """Check if HA version is valid for this integration."""
    return is_min_ha_version(MIN_HA_MAJ_VER, MIN_HA_MIN_VER)


def _notify_message(
    hass: HomeAssistant, notification_id: str, title: str, message: str
) -> None:
    """Notify user with persistent notification."""
    hass.async_create_task(
        hass.services.async_call(
            domain="persistent_notification",
            service="create",
            service_data={
                "title": title,
                "message": message,
                "notification_id": f"{DOMAIN}.{notification_id}",
            },
        )
    )


def _load_option_list(src_list):
    """Load list parameters in JSON from configuration.yaml."""

    if src_list is None:
        return None
    if isinstance(src_list, dict):
        return src_list

    result = {}
    try:
        result = json.loads(src_list)
    except TypeError:
        _LOGGER.error("Invalid format parameter: %s", str(src_list))
    return result


def token_file_name(hostname: str) -> str:
    """Return token file name."""
    return f"{DOMAIN}_{hostname}_token"


def _remove_token_file(hass, hostname, token_file=None):
    """Try to remove token file."""
    if not token_file:
        token_file = hass.config.path(STORAGE_DIR, token_file_name(hostname))

    if os.path.isfile(token_file):
        try:
            os.remove(token_file)
        except Exception as exc:  # pylint: disable=broad-except
            _LOGGER.error(
                "Samsung TV - Error deleting token file %s: %s", token_file, str(exc)
            )


def _migrate_token(hass: HomeAssistant, entry: ConfigEntry, hostname: str) -> None:
    """Migrate token from old file to registry entry."""
    token_file = hass.config.path(STORAGE_DIR, token_file_name(hostname))
    if not os.path.isfile(token_file):
        token_file = (
            os.path.dirname(os.path.realpath(__file__)) + f"/token-{hostname}.txt"
        )
        if not os.path.isfile(token_file):
            return

    try:
        with open(token_file, "r", encoding="utf-8") as os_token_file:
            token = os_token_file.readline()
    except Exception as exc:  # pylint: disable=broad-except
        _LOGGER.error("Error reading token file %s: %s", token_file, str(exc))
        return

    if not token:
        _LOGGER.warning("No token found inside token file %s", token_file)
        return

    hass.config_entries.async_update_entry(
        entry, data={**entry.data, CONF_TOKEN: token}
    )
    _remove_token_file(hass, hostname, token_file)


@callback
def _migrate_options_format(hass: HomeAssistant, entry: ConfigEntry) -> None:
    """Migrate options to new format."""
    opt_migrated = False
    new_options = {}

    for key, option in entry.options.items():
        if key in [CONF_SYNC_TURN_OFF, CONF_SYNC_TURN_ON]:
            if isinstance(option, str):
                new_options[key] = option.split(",")
                opt_migrated = True
                continue
        new_options[key] = option

    # load the option lists in entry option
    yaml_opt = hass.data.get(DOMAIN, {}).get(entry.entry_id, {}).get(DATA_CFG_YAML, {})
    for key in [CONF_APP_LIST, CONF_CHANNEL_LIST, CONF_SOURCE_LIST]:
        if key not in new_options:  # import will occurs only on first restart
            if option := _load_option_list(yaml_opt.get(key, {})):
                message = (
                    f"Configuration key '{key}' has been in imported in integration options,"
                    " you can now remove from configuration.yaml"
                )
                _notify_message(
                    hass, f"config-import-{key}", "SamsungTV Smart", message
                )
                _LOGGER.warning(message)
            new_options[key] = option
            opt_migrated = True

    if opt_migrated:
        hass.config_entries.async_update_entry(entry, options=new_options)


@callback
def _migrate_entry_unique_id(hass: HomeAssistant, entry: ConfigEntry) -> None:
    """Migrate unique_is to new format."""
    if CONF_ID in entry.data:
        new_unique_id = entry.data[CONF_ID]
    elif CONF_MAC in entry.data:
        new_unique_id = entry.data[CONF_MAC]
    else:
        new_unique_id = entry.data[CONF_HOST]

    if entry.unique_id == new_unique_id:
        return

    entries_list = hass.config_entries.async_entries(DOMAIN)
    for other_entry in entries_list:
        if other_entry.unique_id == new_unique_id:
            _LOGGER.warning(
                "Found duplicated entries %s and %s that refer to the same device."
                " Please remove unused entry",
                entry.data[CONF_HOST],
                other_entry.data[CONF_HOST],
            )
            return

    _LOGGER.info(
        "Migrated entry unique id from %s to %s", entry.unique_id, new_unique_id
    )
    hass.config_entries.async_update_entry(entry, unique_id=new_unique_id)


@callback
def _migrate_smartthings_config(hass: HomeAssistant, entry: ConfigEntry) -> None:
    """Migrate smartthings entry usage configuration."""
    if CONF_USE_ST_INT_API_KEY not in entry.data:
        return

    new_data = entry.data.copy()
    use_st = new_data.pop(CONF_USE_ST_INT_API_KEY)
    if use_st:
        if entries_list := hass.config_entries.async_entries(ST_DOMAIN, False, False):
            new_data[CONF_ST_ENTRY_UNIQUE_ID] = entries_list[0].unique_id

    hass.config_entries.async_update_entry(entry, data=new_data)


@callback
def get_smartthings_entries(hass: HomeAssistant) -> dict[str, str] | None:
    """Get the smartthing integration configured entries."""
    entries_list = hass.config_entries.async_entries(ST_DOMAIN, False, False)
    if not entries_list:
        return None

    return {
        entry.unique_id: entry.title
        for entry in entries_list
        if CONF_TOKEN in entry.data
    }


@callback
def get_smartthings_api_key(hass: HomeAssistant, st_unique_id: str) -> str | None:
    """Get the smartthing integration configured API key."""
    entries_list = hass.config_entries.async_entries(ST_DOMAIN, False, False)
    if not entries_list:
        return None

    for entry in entries_list:
        if entry.unique_id == st_unique_id:
            config_data = entry.data
            if CONF_TOKEN not in config_data:
                return None
            return config_data[CONF_TOKEN].get(CONF_ACCESS_TOKEN)

    return None


async def _register_logo_paths(hass: HomeAssistant) -> str | None:
    """Register paths for local logos."""

    static_logo_path = Path(__file__).parent / "static"
    static_paths = [
        StaticPathConfig(
            STATIC_IMAGE_BASE_URL, str(static_logo_path), cache_headers=False
        )
    ]

    local_logo_path = Path(hass.config.path("www", f"{DOMAIN}_logos"))
    url_logo_path = str(local_logo_path)
    if not local_logo_path.exists():
        try:
            local_logo_path.mkdir(parents=True)
        except Exception as exc:  # pylint: disable=broad-except
            _LOGGER.warning(
                "Error registering custom logo folder %s: %s", str(local_logo_path), exc
            )
            url_logo_path = None

    if url_logo_path is not None:
        static_paths.append(
            StaticPathConfig(CUSTOM_IMAGE_BASE_URL, url_logo_path, cache_headers=False)
        )

    await hass.http.async_register_static_paths(static_paths)
    return url_logo_path


async def get_device_info(hostname: str, session: ClientSession) -> dict:
    """Try retrieve device information"""
    try:
        async with async_timeout.timeout(2):
            async with session.get(
                tv_url(host=hostname), raise_for_status=True
            ) as resp:
                info = await resp.json()
    except (asyncio.TimeoutError, ClientConnectionError):
        _LOGGER.warning("Error getting HTTP device info for TV: %s", hostname)
        return {}

    device = info.get("device")
    if not device:
        _LOGGER.warning("Error getting HTTP device info for TV: %s", hostname)
        return {}

    result = {
        key: device[value] for key, value in DEVICE_INFO.items() if value in device
    }

    if ATTR_DEVICE_ID in result:
        device_id = result[ATTR_DEVICE_ID]
        if device_id.startswith("uuid:"):
            result[ATTR_DEVICE_ID] = device_id[len("uuid:") :]

    return result


class SamsungTVInfo:
    """Class to connect and collect TV information."""

    def __init__(self, hass, hostname, ws_name):
        """Initialize the object."""
        self._hass = hass
        self._hostname = hostname
        self._ws_name = ws_name
        self._ws_port = 0
        self._ws_token = None
        self._ping_port = None

    @property
    def ws_port(self):
        """Return used WebSocket port."""
        return self._ws_port

    @property
    def ws_token(self):
        """Return WebSocket token."""
        return self._ws_token

    @property
    def ping_port(self):
        """Return the port used to ping the TV."""
        return self._ping_port

    def _try_connect_ws(self):
        """Try to connect to device using web sockets on port 8001 and 8002"""

        self._ping_port = SamsungTVWS.ping_probe(self._hostname)
        if self._ping_port is None:
            _LOGGER.error(
                "Connection to SamsungTV %s failed. Check that TV is on", self._hostname
            )
            return RESULT_NOT_SUCCESSFUL

        if self._ws_port and self._ws_token:
            port_list = tuple([self._ws_port, 8001, 8002])
        else:
            port_list = (8001, 8002)

        for index, port in enumerate(port_list):

            timeout = 45  # We need this high timeout because waiting for TV auth popup
            token = None
            if len(port_list) > 2 and index == 0:
                timeout = DEFAULT_TIMEOUT
                token = self._ws_token

            try:
                _LOGGER.info(
                    "Try to configure SamsungTV %s using port %s%s",
                    self._hostname,
                    str(port),
                    " with existing token" if token else "",
                )
                with SamsungTVWS(
                    name=f"{WS_PREFIX} {self._ws_name}",  # this is the name shown in the TV
                    host=self._hostname,
                    port=port,
                    token=token,
                    timeout=timeout,
                ) as remote:
                    remote.open()
                    self._ws_token = remote.token
                _LOGGER.info("Found working configuration using port %s", str(port))
                self._ws_port = port
                return RESULT_SUCCESS
            except (OSError, ConnectionFailure, WebSocketException) as err:
                _LOGGER.info(
                    "Configuration failed using port %s, error: %s", str(port), err
                )

        _LOGGER.error("Web socket connection to SamsungTV %s failed", self._hostname)
        return RESULT_NOT_SUCCESSFUL

    @staticmethod
    async def _try_connect_st(api_key, device_id, session: ClientSession):
        """Try to connect to ST device"""

        try:
            async with async_timeout.timeout(10):
                _LOGGER.info("Try connection to SmartThings TV with id [%s]", device_id)
                with SmartThingsTV(
                    api_key=api_key,
                    device_id=device_id,
                    session=session,
                ) as st_tv:
                    result = await st_tv.async_device_health()
                if result:
                    _LOGGER.info("Connection completed successfully.")
                    return RESULT_SUCCESS
                _LOGGER.error("Connection to SmartThings TV not available.")
                return RESULT_ST_DEVICE_NOT_FOUND
        except ClientResponseError as err:
            _LOGGER.error("Failed connecting to SmartThings TV, error: %s", err)
            if err.status == 400:  # Bad request, means that token is valid
                return RESULT_ST_DEVICE_NOT_FOUND
        except Exception as err:  # pylint: disable=broad-except
            _LOGGER.error("Failed connecting with SmartThings, error: %s", err)

        return RESULT_WRONG_APIKEY

    @staticmethod
    async def get_st_devices(api_key, session: ClientSession, st_device_label=""):
        """Get list of available ST devices"""

        try:
            async with async_timeout.timeout(4):
                devices = await SmartThingsTV.get_devices_list(
                    api_key, session, st_device_label
                )
        except Exception as err:  # pylint: disable=broad-except
            _LOGGER.error("Failed connecting with SmartThings, error: %s", err)
            return None

        return devices

    async def try_connect(
        self,
        session: ClientSession,
        api_key=None,
        st_device_id=None,
        *,
        ws_port=None,
        ws_token=None,
    ):
        """Try connect device"""
        if session is None:
            return RESULT_NOT_SUCCESSFUL

        if ws_port and ws_token:
            self._ws_port = ws_port
            self._ws_token = ws_token

        result = await self._hass.async_add_executor_job(self._try_connect_ws)
        if result == RESULT_SUCCESS:
            if api_key and st_device_id:
                result = await self._try_connect_st(api_key, st_device_id, session)

        return result


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up the Samsung TV integration."""
    if not is_valid_ha_version():
        msg = (
            "This integration require at least HomeAssistant version"
            f" {__min_ha_version__}, you are running version {__version__}."
            " Please upgrade HomeAssistant to continue use this integration."
        )
        _notify_message(hass, "inv_ha_version", "SamsungTV Smart", msg)
        _LOGGER.warning(msg)
        return True

    if DOMAIN in config:
        entries_list = hass.config_entries.async_entries(DOMAIN)
        for entry_config in config[DOMAIN]:
            # get ip address
            ip_address = entry_config[CONF_HOST]

            # check if already configured
            valid_entries = [
                entry.entry_id
                for entry in entries_list
                if entry.data[CONF_HOST] == ip_address
            ]
            if not valid_entries:
                _LOGGER.warning(
                    "Found yaml configuration for not configured device %s."
                    " Please use UI to configure",
                    ip_address,
                )
                continue

            data_yaml = {
                key: value
                for key, value in entry_config.items()
                if key in SAMSMART_SCHEMA and value
            }
            if data_yaml:
                if DOMAIN not in hass.data:
                    hass.data[DOMAIN] = {}
                hass.data[DOMAIN][valid_entries[0]] = {DATA_CFG_YAML: data_yaml}

    # Register path for local logo
    if local_logo_path := await _register_logo_paths(hass):
        hass.data.setdefault(DOMAIN, {})[LOCAL_LOGO_PATH] = local_logo_path

    return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Set up the Samsung TV platform."""
    if not is_valid_ha_version():
        return False

    # migrate unique id to a accepted format
    _migrate_entry_unique_id(hass, entry)

    # migrate smartthings entry usage configuration
    _migrate_smartthings_config(hass, entry)

    # migrate old token file to registry entry if required
    if CONF_TOKEN not in entry.data:
        await hass.async_add_executor_job(
            _migrate_token, hass, entry, entry.data[CONF_HOST]
        )

    # migrate options to new format if required
    _migrate_options_format(hass, entry)

    # setup entry
    if DOMAIN not in hass.data:
        hass.data[DOMAIN] = {}

    add_conf = None
    config = entry.data.copy()
    if entry.entry_id in hass.data[DOMAIN]:
        add_conf = hass.data[DOMAIN][entry.entry_id].get(DATA_CFG_YAML, {})
        for attr, value in add_conf.items():
            if value:
                config[attr] = value

    # setup entry
    hass.data[DOMAIN][entry.entry_id] = {
        DATA_CFG: config,
        DATA_OPTIONS: entry.options.copy(),
    }
    if add_conf:
        hass.data[DOMAIN][entry.entry_id][DATA_CFG_YAML] = add_conf
    entry.async_on_unload(entry.add_update_listener(_update_listener))

    await hass.config_entries.async_forward_entry_setups(entry, SAMSMART_PLATFORM)

    return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Unload a config entry."""
    if unload_ok := await hass.config_entries.async_unload_platforms(
        entry, SAMSMART_PLATFORM
    ):
        hass.data[DOMAIN][entry.entry_id].pop(DATA_CFG)
        hass.data[DOMAIN][entry.entry_id].pop(DATA_OPTIONS)
        if not hass.data[DOMAIN][entry.entry_id]:
            hass.data[DOMAIN].pop(entry.entry_id)

    return unload_ok


async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
    """Remove a config entry."""
    await hass.async_add_executor_job(_remove_token_file, hass, entry.data[CONF_HOST])
    if DOMAIN in hass.data:
        hass.data[DOMAIN].pop(entry.entry_id, None)
        if not hass.data[DOMAIN]:
            hass.data.pop(DOMAIN)


async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
    """Update when config_entry options update."""
    hass.data[DOMAIN][entry.entry_id][DATA_OPTIONS] = entry.options.copy()
    async_dispatcher_send(hass, SIGNAL_CONFIG_ENTITY)


================================================
FILE: custom_components/samsungtv_smart/api/__init__.py
================================================
"""SamsungTV Smart TV WS API library."""


================================================
FILE: custom_components/samsungtv_smart/api/samsungcast.py
================================================
"""Smartthings TV integration cast tube."""

from __future__ import annotations

import logging
import xml.etree.ElementTree as ET

from casttube import YouTubeSession
import requests

_LOGGER = logging.getLogger(__name__)


def _format_url(host: str, app: str) -> str:
    """Return URL used to retrieve screen id."""
    return f"http://{host}:8080/ws/app/{app}"


class CastTubeNotSupported(Exception):
    """Error during cast."""


class SamsungCastTube:
    """Class to cast video to youtube TV app."""

    def __init__(self, host: str):
        """Initialize the class."""
        self._host = host
        self._cast_api: YouTubeSession | None = None

    @staticmethod
    def _get_screen_id(host: str) -> str:
        """Retrieve screen id from the TV."""
        url = _format_url(host, "YouTube")
        try:
            response = requests.get(url, timeout=5)
        except requests.ConnectionError as exc:
            _LOGGER.warning(
                "Failed to retrieve YouTube screenID for host %s: %s", host, exc
            )
            raise CastTubeNotSupported() from exc

        screen_id = None
        tree = ET.fromstring(response.content.decode("utf8"))
        for elem in tree.iter():
            if elem.tag.endswith("screenId"):
                _LOGGER.debug("YouTube ScreenID: %s", screen_id)
                screen_id = elem.text

        if screen_id is None:
            _LOGGER.warning("Failed to retrieve YouTube screenID for host %s", host)
            raise CastTubeNotSupported()

        return screen_id

    def _get_api(self) -> YouTubeSession:
        """Get the API to cast video."""
        if not self._cast_api:
            screen_id = self._get_screen_id(self._host)
            self._cast_api = YouTubeSession(screen_id)
        return self._cast_api

    def play_video(self, video_id: str) -> None:
        """Play video_id immediatly."""
        self._get_api().play_video(video_id)

    def play_next(self, video_id: str) -> None:
        """Play video_id after the currently playing video.."""
        self._get_api().play_next(video_id)

    def add_to_queue(self, video_id: str) -> None:
        """Add a video to the video queue."""
        self._get_api().add_to_queue(video_id)

    def clear_queue(self) -> None:
        """Clear the video queue."""
        self._get_api().clear_playlist()


================================================
FILE: custom_components/samsungtv_smart/api/samsungws.py
================================================
"""
SamsungTVWS - Samsung Smart TV WS API wrapper

Copyright (C) 2019 Xchwarze
Copyright (C) 2020 Ollo69

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor,
    Boston, MA  02110-1335  USA

"""

from __future__ import annotations

import base64
from datetime import datetime, timezone
from enum import Enum
import json
import logging
import socket
import ssl
import subprocess
import sys
from threading import Lock, Thread
import time
from typing import Any
from urllib.parse import urlencode, urljoin
import uuid

import aiohttp
import requests
import websocket

from .shortcuts import SamsungTVShortcuts

DEFAULT_POWER_ON_DELAY = 120
MIN_APP_SCAN_INTERVAL = 9
MAX_APP_VALIDITY_SEC = 60
MAX_WS_PING_INTERVAL = 10
PING_TIMEOUT = 3
TYPE_DEEP_LINK = "DEEP_LINK"
TYPE_NATIVE_LAUNCH = "NATIVE_LAUNCH"

_WS_ENDPOINT_REMOTE_CONTROL = "/api/v2/channels/samsung.remote.control"
_WS_ENDPOINT_APP_CONTROL = "/api/v2"
_WS_ENDPOINT_ART = "/api/v2/channels/com.samsung.art-app"
_WS_LOG_NAME = "websocket"

_LOG_PING_PONG = False
_LOGGING = logging.getLogger(__name__)


def _set_ws_logger_level(level: int = logging.CRITICAL) -> None:
    """Set the websocket library logging level."""
    ws_logger = logging.getLogger(_WS_LOG_NAME)
    if ws_logger.level < level:
        ws_logger.setLevel(level)


def _format_rest_url(host: str, append: str = "") -> str:
    """Return URL used for rest commands."""
    return f"http://{host}:8001/api/v2/{append}"


def gen_uuid() -> str:
    """Generate new uuid."""
    return str(uuid.uuid4())


def aware_utcnow() -> datetime:
    """Return current UTC time with timezone info."""
    return datetime.now(timezone.utc)


def kill_subprocess(
    process: subprocess.Popen[Any],
) -> None:
    """Force kill a subprocess and wait for it to exit."""
    process.kill()
    process.communicate()
    process.wait()

    del process


def _process_api_response(response, *, raise_error=True):
    """Process response received by TV."""
    try:
        return json.loads(response)
    except json.JSONDecodeError as exc:
        _LOGGING.debug("Failed to parse response from TV. response text: %s", response)
        if raise_error:
            raise ResponseError(
                "Failed to parse response from TV. Maybe feature not supported on this model"
            ) from exc
    return response


def _log_ping_pong(msg, *args):
    """Log ping pong message if enabled."""
    if not _LOG_PING_PONG:
        return
    _LOGGING.debug(msg=msg, args=args)


class Ping:
    """Class for handling Ping to a specific host."""

    def __init__(self, host):
        """Initialize the object."""
        self._ip_address = host
        if sys.platform == "win32":
            self._ping_cmd = ["ping", "-n", "1", "-w", "2000", host]
        else:
            self._ping_cmd = ["ping", "-n", "-q", "-c1", "-W2", host]

    def ping(self, port=0):
        """Check if IP is available using ICMP or trying open a specific port."""
        if port > 0:
            return self._ping_socket(port)
        return self._ping()

    def _ping(self):
        """Send ICMP echo request and return True if success."""
        with subprocess.Popen(
            self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
        ) as pinger:
            try:
                pinger.communicate(timeout=1 + PING_TIMEOUT)
                return pinger.returncode == 0
            except subprocess.TimeoutExpired:
                kill_subprocess(pinger)
                return False
            except subprocess.CalledProcessError:
                return False

    def _ping_socket(self, port):
        """Check if port is available and return True if success."""
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.settimeout(PING_TIMEOUT - 1)
            return sock.connect_ex((self._ip_address, port)) == 0


class ConnectionFailure(Exception):
    """Error during connection."""


class ResponseError(Exception):
    """Error in response."""


class HttpApiError(Exception):
    """Error using HTTP API."""


class App:
    """Define a TV Application."""

    def __init__(self, app_id, app_name, app_type):
        self.app_id = app_id
        self.app_name = app_name
        self.app_type = app_type


class ArtModeStatus(Enum):
    """Define possible ArtMode status."""

    Unsupported = 0
    Unavailable = 1
    Off = 2
    On = 3


class SamsungTVAsyncRest:
    """Class that implement rest request in async."""

    def __init__(
        self,
        host: str,
        session: aiohttp.ClientSession,
        timeout=None,
    ) -> None:
        """Initialize the class."""
        self._host = host
        self._session = session
        self._timeout = None if timeout == 0 else timeout

    async def _rest_request(self, target: str, method: str = "GET") -> dict[str, Any]:
        """Perform async rest request."""
        url = _format_rest_url(self._host, target)
        try:
            if method == "POST":
                req = self._session.post(url, timeout=self._timeout, verify_ssl=False)
            elif method == "PUT":
                req = self._session.put(url, timeout=self._timeout, verify_ssl=False)
            elif method == "DELETE":
                req = self._session.delete(url, timeout=self._timeout, verify_ssl=False)
            else:
                req = self._session.get(url, timeout=self._timeout, verify_ssl=False)
            async with req as resp:
                return _process_api_response(await resp.text())
        except aiohttp.ClientConnectionError as ex:
            raise HttpApiError(
                "TV unreachable or feature not supported on this model."
            ) from ex

    async def async_rest_device_info(self) -> dict[str, Any]:
        """Get device info using rest api call."""
        _LOGGING.debug("Get device info via rest api")
        return await self._rest_request("")

    async def async_rest_app_status(self, app_id: str) -> dict[str, Any]:
        """Get app status using rest api call."""
        _LOGGING.debug("Get app %s status via rest api", app_id)
        return await self._rest_request("applications/" + app_id)

    async def async_rest_app_run(self, app_id: str) -> dict[str, Any]:
        """Run an app using rest api call."""
        _LOGGING.debug("Run app %s via rest api", app_id)
        return await self._rest_request("applications/" + app_id, "POST")

    async def async_rest_app_close(self, app_id: str) -> dict[str, Any]:
        """Close an app using rest api call."""
        _LOGGING.debug("Close app %s via rest api", app_id)
        return await self._rest_request("applications/" + app_id, "DELETE")

    async def async_rest_app_install(self, app_id: str) -> dict[str, Any]:
        """Install a new app using rest api call."""
        _LOGGING.debug("Install app %s via rest api", app_id)
        return await self._rest_request("applications/" + app_id, "PUT")


class SamsungTVWS:
    """Class to manage websocket communication with tizen TV."""

    def __init__(
        self,
        host: str,
        *,
        token: str | None = None,
        token_file: str | None = None,
        port: int | None = 8001,
        timeout: int | None = None,
        key_press_delay: float | None = 1.0,
        name: str | None = "SamsungTvRemote",
        app_list: dict | None = None,
        ping_port: int | None = 0,
    ):
        """Initialize SamsungTVWS object."""
        self.host = host
        self.token = token
        self.token_file = token_file
        self.port = port or 8001
        self.timeout = None if timeout == 0 else timeout
        self.key_press_delay = 1.0 if key_press_delay is None else key_press_delay
        self.name = name or "SamsungTvRemote"
        self._app_list = dict(app_list) if app_list else None
        self._ping_port = ping_port or 0

        self.connection = None
        self._artmode_status = ArtModeStatus.Unsupported
        self._power_on_requested = False
        self._power_on_requested_time = datetime.min
        self._power_on_delay = DEFAULT_POWER_ON_DELAY
        self._power_on_artmode = False

        self._installed_app = {}
        self._running_apps: dict[str, datetime] = {}
        self._running_app: str | None = None
        self._running_app_changed: bool | None = None
        self._last_running_scan = aware_utcnow()
        self._app_type = {}
        self._sync_lock = Lock()
        self._last_app_scan = datetime.min

        self._ping_thread = None
        self._ping_thread_run = False

        self._ws_remote = None
        self._client_remote = None
        self._last_ping = datetime.min
        self._is_connected = False

        self._ws_control = None
        self._client_control = None
        self._last_control_ping = datetime.min
        self._is_control_connected = False

        self._ws_art = None
        self._client_art = None
        self._last_art_ping = datetime.min
        self._client_art_supported = 2

        self._ping = Ping(self.host)
        self._status_callback = None
        self._new_token_callback = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.close()

    @staticmethod
    def ping_probe(host):
        """Try to ping device and return usable port."""
        ping = Ping(host)
        for port in (9197, 0):
            try:
                if ping.ping(port):
                    return port
            except Exception:  # pylint: disable=broad-except
                _LOGGING.debug("Failed to ping device using port %s", port)

        return None

    @staticmethod
    def _serialize_string(string):
        if isinstance(string, str):
            string = str.encode(string)
        return base64.b64encode(string).decode("utf-8")

    def _is_ssl_connection(self):
        return self.port == 8002

    def _format_websocket_url(self, path, is_ssl=False, use_token=True):
        scheme = "wss" if is_ssl else "ws"

        base_uri = f"{scheme}://{self.host}:{self.port}"
        ws_uri = urljoin(base_uri, path)
        query = {"name": self._serialize_string(self.name)}
        if is_ssl and use_token:
            if token := self._get_token():
                query["token"] = token
        ws_query = urlencode(query)
        return f"{ws_uri}?{ws_query}"

    def set_ping_port(self, port: int):
        """Set a new ping port."""
        self._ping_port = port

    def update_app_list(self, app_list: dict | None):
        """Update application list."""
        self._app_list = dict(app_list) if app_list else None

    def register_new_token_callback(self, func):
        """Register a callback function."""
        self._new_token_callback = func

    def register_status_callback(self, func):
        """Register callback function used on status change."""
        self._status_callback = func

    def unregister_status_callback(self):
        """Unregister callback function used on status change."""
        self._status_callback = None

    def _get_token(self):
        """Get current token."""
        if self.token_file is not None:
            try:
                with open(self.token_file, "r", encoding="utf-8") as token_file:
                    return token_file.readline()
            except Exception as exc:  # pylint: disable=broad-except
                _LOGGING.error("Failed to read TV token file: %s", str(exc))
                return ""
        return self.token

    def _set_token(self, token):
        """Save new token."""
        _LOGGING.debug("New token %s", token)
        if self.token_file is not None:
            _LOGGING.debug("Save new token to file %s", self.token_file)
            with open(self.token_file, "w", encoding="utf-8") as token_file:
                token_file.write(token)
            return

        if self.token is not None and self.token == token:
            return
        self.token = token
        if self._new_token_callback is not None:
            self._new_token_callback()

    def _ws_send(
        self,
        command,
        key_press_delay=None,
        *,
        use_control=False,
        ws_socket=None,
        raise_on_closed=False,
    ):
        """Send a command using the appropriate websocket."""
        using_remote = False
        if not use_control:
            if self._ws_remote:
                connection = self._ws_remote
                using_remote = True
            else:
                connection = self.open()
        elif ws_socket:
            connection = ws_socket
        else:
            self._start_client(start_all=True)
            return False

        payload = json.dumps(command)
        try:
            connection.send(payload)
        except websocket.WebSocketConnectionClosedException:
            if raise_on_closed:
                raise
            _LOGGING.warning("_ws_send: connection is closed, send command failed")
            if using_remote or use_control:
                _LOGGING.info("_ws_send: try to restart communication threads")
                self._start_client(start_all=use_control)
            return False
        except websocket.WebSocketTimeoutException:
            _LOGGING.warning("_ws_send: timeout error sending command %s", payload)
            return False

        if using_remote:
            # we consider a message sent valid as a ping
            self._last_ping = aware_utcnow()

        if key_press_delay is None:
            if self.key_press_delay > 0:
                time.sleep(self.key_press_delay)
        elif key_press_delay > 0:
            time.sleep(key_press_delay)

        return True

    def _rest_request(self, target, method="GET"):
        """Send a rest command using http protocol."""
        url = _format_rest_url(self.host, target)
        try:
            if method == "POST":
                response = requests.post(url, timeout=self.timeout)
            elif method == "PUT":
                response = requests.put(url, timeout=self.timeout)
            elif method == "DELETE":
                response = requests.delete(url, timeout=self.timeout)
            else:
                response = requests.get(url, timeout=self.timeout)
        except requests.ConnectionError as exc:
            raise HttpApiError(
                "TV unreachable or feature not supported on this model."
            ) from exc
        return _process_api_response(response.text, raise_error=False)

    def _check_conn_id(self, resp_data):
        """Check if returned connection id from WS server is valid for this TV."""
        if not resp_data:
            return False

        msg_id = resp_data.get("id")
        if not msg_id:
            return False

        clients_info = resp_data.get("clients")
        for client in clients_info:
            device_name = client.get("deviceName")
            if device_name:
                if device_name == self._serialize_string(self.name):
                    conn_id = client.get("id", "")
                    if conn_id == msg_id:
                        return True
        return False

    @staticmethod
    def _run_forever(
        ws_app: websocket.WebSocketApp, *, sslopt: dict = None, ping_interval: int = 0
    ) -> None:
        """Call method run_forever changing library log level before."""
        _set_ws_logger_level()
        ws_app.run_forever(sslopt=sslopt, ping_interval=ping_interval)

    def _client_remote_thread(self):
        """Start the main client WS thread that connect to the remote TV."""
        if self._ws_remote:
            return

        is_ssl = self._is_ssl_connection()
        url = self._format_websocket_url(_WS_ENDPOINT_REMOTE_CONTROL, is_ssl=is_ssl)
        sslopt = {"cert_reqs": ssl.CERT_NONE} if is_ssl else {}

        websocket.setdefaulttimeout(self.timeout)
        self._ws_remote = websocket.WebSocketApp(
            url,
            on_message=self._on_message_remote,
            on_ping=self._on_ping_remote,
        )
        _LOGGING.debug("Thread SamsungRemote started")
        # we set ping interval (1 hour) only to enable multi-threading mode
        # on socket. TV do not answer to ping but send ping to client
        self._run_forever(self._ws_remote, sslopt=sslopt, ping_interval=3600)
        self._is_connected = False
        if self._status_callback is not None:
            self._status_callback()
        if self._ws_art:
            self._ws_art.close()
        if self._ws_control:
            self._ws_control.close()
        self._ws_remote.close()
        self._ws_remote = None
        _LOGGING.debug("Thread SamsungRemote terminated")

    def _on_ping_remote(self, _, payload):
        """Manage ping message received by remote WS connection."""
        _log_ping_pong("Received WS remote ping %s, sending pong", payload)
        self._last_ping = aware_utcnow()
        if self._ws_remote.sock:
            try:
                self._ws_remote.sock.pong(payload)
            except Exception as ex:  # pylint: disable=broad-except
                _LOGGING.warning("WS remote send_pong failed, %s", ex)

    def _on_message_remote(self, _, message):
        """Manage messages received by remote WS connection."""
        response = _process_api_response(message)
        _LOGGING.debug(response)
        event = response.get("event")
        if not event:
            return

        # we consider a message valid as a ping
        self._last_ping = aware_utcnow()

        if event == "ms.channel.connect":
            conn_data = response.get("data")
            if not self._check_conn_id(conn_data):
                return
            _LOGGING.debug("Message remote: received connect")
            token = conn_data.get("token")
            if token:
                self._set_token(token)
            self._is_connected = True
            self._request_apps_list()
            self._start_client(start_all=True)
            if self._status_callback is not None:
                self._status_callback()
        elif event == "ed.installedApp.get":
            _LOGGING.debug("Message remote: received installedApp")
            self._handle_installed_app(response)
        elif event == "ed.edenTV.update":
            _LOGGING.debug("Message remote: received edenTV")
            self._get_running_app(force_scan=True)

    def _request_apps_list(self):
        """Request to the TV the list of installed apps."""
        _LOGGING.debug("Request app list")
        self._ws_send(
            {
                "method": "ms.channel.emit",
                "params": {"event": "ed.installedApp.get", "to": "host"},
            },
            key_press_delay=0,
        )

    def _handle_installed_app(self, response):
        """Manage the list of installed apps received from the TV."""
        list_app = response.get("data", {}).get("data")
        installed_app = {}
        for app_info in list_app:
            app_id = app_info["appId"]
            _LOGGING.debug("Found app: %s", app_id)
            app = App(app_id, app_info["name"], app_info["app_type"])
            installed_app[app_id] = app
        self._installed_app = installed_app

    def _client_control_thread(self):
        """Start the client control WS thread used to manage running apps."""
        if self._ws_control:
            return

        is_ssl = self._is_ssl_connection()
        url = self._format_websocket_url(
            _WS_ENDPOINT_APP_CONTROL, is_ssl=is_ssl, use_token=False
        )
        sslopt = {"cert_reqs": ssl.CERT_NONE} if is_ssl else {}

        websocket.setdefaulttimeout(self.timeout)
        self._ws_control = websocket.WebSocketApp(
            url,
            on_message=self._on_message_control,
            on_ping=self._on_ping_control,
        )
        _LOGGING.debug("Thread SamsungControl started")
        # we set ping interval (1 hour) only to enable multi-threading mode
        # on socket. TV do not answer to ping but send ping to client
        self._run_forever(self._ws_control, sslopt=sslopt, ping_interval=3600)
        self._is_control_connected = False
        self._ws_control.close()
        self._ws_control = None
        self._running_app_changed = None
        _LOGGING.debug("Thread SamsungControl terminated")

    def _on_ping_control(self, _, payload):
        """Manage ping message received by control WS channel."""
        _log_ping_pong("Received WS control ping %s, sending pong", payload)
        self._last_control_ping = aware_utcnow()
        if self._ws_control.sock:
            try:
                self._ws_control.sock.pong(payload)
            except Exception as ex:  # pylint: disable=broad-except
                _LOGGING.warning("WS control send_pong failed, %s", ex)

    def _on_message_control(self, _, message):
        """Manage messages received by control WS channel."""
        response = _process_api_response(message)
        _LOGGING.debug(response)
        result = response.get("result")
        if result:
            self._set_running_app(response)
            return
        error = response.get("error")
        if error:
            self._manage_control_err(response)
            return
        event = response.get("event")
        if not event:
            return
        if event == "ms.channel.connect":
            conn_data = response.get("data")
            if not self._check_conn_id(conn_data):
                return
            _LOGGING.debug("Message control: received connect")
            self._is_control_connected = True
            self._get_running_app()
        elif event == "ed.installedApp.get":
            _LOGGING.debug("Message control: received installedApp")
            self._handle_installed_app(response)

    def _set_running_app(self, response):
        """Set the current running app based on received message."""
        if not (app_id := response.get("id")):
            return
        if (result := response.get("result")) is None:
            return
        if isinstance(result, bool):
            is_running = result
        elif (is_running := result.get("visible")) is None:
            return

        call_time = aware_utcnow()
        self._last_running_scan = call_time
        self._running_apps[app_id] = call_time
        if self._running_app:
            if is_running and app_id != self._running_app:
                _LOGGING.debug("app running: %s", app_id)
                self._running_app = app_id
                self._running_app_changed = True
            elif not is_running and app_id == self._running_app:
                _LOGGING.debug("app stopped: %s", app_id)
                self._running_app = None
                self._running_app_changed = True
        elif is_running:
            _LOGGING.debug("app running: %s", app_id)
            self._running_app = app_id
            self._running_app_changed = True

        if self._running_app_changed is None:
            self._running_app_changed = True

    def _manage_control_err(self, response):
        """Manage errors from control WS channel."""
        app_id = response.get("id")
        if not app_id:
            return
        error_code = response.get("error", {}).get("code", 0)
        if error_code == 404:  # Not found error
            if self._installed_app:
                if app_id not in self._installed_app:
                    _LOGGING.error("App ID %s not found", app_id)
                return
            # app_type = self._app_type.get(app_id)
            # if app_type is None:
            #     _LOGGING.info(
            #         "App ID %s with type DEEP_LINK not found, set as NATIVE_LAUNCH",
            #         app_id,
            #     )
            #     self._app_type[app_id] = 4

    def _get_app_status(self, app_id, app_type):
        """Send a message to control WS channel to get the app status."""
        _LOGGING.debug("Get app status: AppID: %s, AppType: %s", app_id, app_type)

        if not (self._ws_control and self._is_control_connected):
            return

        if app_type == 4:  # app type 4 always return not found error
            return

        method = "ms.application.get"
        try:
            self._ws_send(
                {
                    "id": app_id,
                    "method": method,
                    "params": {"id": app_id},
                },
                key_press_delay=0,
                use_control=True,
                ws_socket=self._ws_control,
                raise_on_closed=True,
            )
        except websocket.WebSocketConnectionClosedException:
            _LOGGING.debug("Get app status aborted: connection closed")

    def _client_art_thread(self):
        """Start the client art WS thread used to manage art mode status."""
        if self._ws_art:
            return

        is_ssl = self._is_ssl_connection()
        url = self._format_websocket_url(
            _WS_ENDPOINT_ART, is_ssl=is_ssl, use_token=False
        )
        sslopt = {"cert_reqs": ssl.CERT_NONE} if is_ssl else {}

        websocket.setdefaulttimeout(self.timeout)
        self._ws_art = websocket.WebSocketApp(
            url,
            on_message=self._on_message_art,
            on_ping=self._on_ping_art,
        )
        _LOGGING.debug("Thread SamsungArt started")
        # we set ping interval (1 hour) only to enable multi-threading mode
        # on socket. TV do not answer to ping but send ping to client
        self._run_forever(self._ws_art, sslopt=sslopt, ping_interval=3600)
        self._ws_art.close()
        self._ws_art = None
        _LOGGING.debug("Thread SamsungArt terminated")

    def _on_ping_art(self, _, payload):
        """Manage ping message received by art WS channel."""
        _log_ping_pong("Received WS art ping %s, sending pong", payload)
        self._last_art_ping = aware_utcnow()
        if self._ws_art.sock:
            try:
                self._ws_art.sock.pong(payload)
            except Exception as ex:  # pylint: disable=broad-except
                _LOGGING.warning("WS art send_pong failed: %s", ex)

    def _on_message_art(self, _, message):
        """Manage messages received by art WS channel."""
        response = _process_api_response(message)
        _LOGGING.debug(response)
        event = response.get("event")
        if not event:
            return

        # we consider a message valid as a ping
        self._last_art_ping = aware_utcnow()

        if event == "ms.channel.connect":
            conn_data = response.get("data")
            if not self._check_conn_id(conn_data):
                return
            _LOGGING.debug("Message art: received connect")
            self._client_art_supported = 1
        elif event == "ms.channel.ready":
            _LOGGING.debug("Message art: channel ready")
            self._get_artmode_status()
        elif event == "d2d_service_message":
            _LOGGING.debug("Message art: d2d message")
            self._handle_artmode_status(response)

    def _get_artmode_status(self):
        """Detect current art mode based on received message."""
        _LOGGING.debug("Sending get_art_status")
        msg_data = {
            "request": "get_artmode_status",
            "id": gen_uuid(),
        }
        self._ws_send(
            {
                "method": "ms.channel.emit",
                "params": {
                    "data": json.dumps(msg_data),
                    "to": "host",
                    "event": "art_app_request",
                },
            },
            key_press_delay=0,
            use_control=True,
            ws_socket=self._ws_art,
        )

    def _handle_artmode_status(self, response):
        """Handle received art mode status."""
        data_str = response.get("data")
        if not data_str:
            return
        data = _process_api_response(data_str)
        event = data.get("event", "")
        if event == "art_mode_changed":
            status = data.get("status", "")
            if status == "on":
                artmode_status = ArtModeStatus.On
            else:
                artmode_status = ArtModeStatus.Off
        elif event == "artmode_status":
            value = data.get("value", "")
            if value == "on":
                artmode_status = ArtModeStatus.On
            else:
                artmode_status = ArtModeStatus.Off
        elif event == "go_to_standby":
            artmode_status = ArtModeStatus.Unavailable
        elif event == "wakeup":
            self._get_artmode_status()
            return
        else:
            # Unknown message
            return

        if self._power_on_requested and artmode_status != ArtModeStatus.Unavailable:
            if artmode_status == ArtModeStatus.On and not self._power_on_artmode:
                self.send_key("KEY_POWER", key_press_delay=0)
            elif artmode_status == ArtModeStatus.Off and self._power_on_artmode:
                self.send_key("KEY_POWER", key_press_delay=0)
            self._power_on_requested = False

        self._artmode_status = artmode_status

    @property
    def is_connected(self):
        """Return if WS connection is open."""
        return self._is_connected

    @property
    def artmode_status(self):
        """Return current art mode status."""
        return self._artmode_status

    @property
    def installed_app(self):
        """Return a list of installed apps."""
        return self._installed_app

    @property
    def running_app(self):
        """Return current running app."""
        return self._running_app

    def is_app_running(self, app_id: str) -> bool | None:
        """Return if app_id is running app."""
        if app_id == self._running_app:
            return True
        if (last_seen := self._running_apps.get(app_id)) is None:
            return None
        app_age = (self._last_running_scan - last_seen).total_seconds()
        if app_age >= MAX_APP_VALIDITY_SEC:
            self._running_apps.pop(app_id)
            return None
        return False

    def _ping_thread_method(self):
        """Start the ping thread that check the TV status."""
        ping = Ping(self.host)
        while self._ping_thread_run:
            if ping.ping(self._ping_port):
                if not self._is_connected:
                    self._start_client()
                else:
                    self._check_remote()
            else:
                if self._is_connected:
                    self.stop_client()
            time.sleep(1.0)

    def _check_remote(self):
        """Check current remote thread status."""
        call_time = aware_utcnow()
        if self._ws_remote:
            difference = (call_time - self._last_ping).total_seconds()
            if difference >= MAX_WS_PING_INTERVAL:
                self.stop_client()
                if self._artmode_status != ArtModeStatus.Unsupported:
                    self._artmode_status = ArtModeStatus.Unavailable
            else:
                self._check_art_mode()
                self._get_running_app()
                self._notify_app_change()

        if self._power_on_requested:
            difference = (call_time - self._power_on_requested_time).total_seconds()
            if difference > self._power_on_delay:
                self._power_on_requested = False

    def _check_art_mode(self):
        """Check current art mode and start related control thread if required."""
        if self._artmode_status == ArtModeStatus.Unsupported:
            return
        if self._ws_art:
            difference = (aware_utcnow() - self._last_art_ping).total_seconds()
            if difference >= MAX_WS_PING_INTERVAL:
                self._artmode_status = ArtModeStatus.Unavailable
                self._ws_art.close()
        elif self._ws_remote:
            self._start_client(start_all=True)

    def _notify_app_change(self):
        """Notify that running app is changed."""
        if not self._running_app_changed:
            return
        if not self._status_callback:
            self._running_app_changed = False
            return
        last_change = (aware_utcnow() - self._last_running_scan).total_seconds()
        if last_change >= 2:  # delay 2 seconds before calling
            self._running_app_changed = False
            self._status_callback()

    def _get_running_app(self, *, force_scan=False):
        """Query current running app using control channel."""
        if not (self._ws_control and self._is_control_connected):
            return

        scan_interval = 1 if force_scan else MIN_APP_SCAN_INTERVAL
        with self._sync_lock:
            call_time = aware_utcnow()
            difference = (call_time - self._last_app_scan).total_seconds()
            if difference < scan_interval:
                return
            self._last_app_scan = call_time

        if self._app_list is not None:
            app_to_check = {}
            for app_name, app_id in self._app_list.items():
                app = None
                if self._installed_app:
                    app = self._installed_app.get(app_id)
                else:
                    app_type = self._app_type.get(app_id, 2)
                    if app_type <= 4:
                        app = App(app_id, app_name, app_type)
                if app:
                    app_to_check[app_id] = app
        else:
            app_to_check = self._installed_app

        for app in app_to_check.values():
            self._get_app_status(app.app_id, app.app_type)

    def set_power_on_request(self, set_art_mode=False, power_on_delay=0):
        """Set a power on request status and save the time of the rquest."""
        self._power_on_requested = True
        self._power_on_requested_time = aware_utcnow()
        self._power_on_artmode = set_art_mode
        self._power_on_delay = max(power_on_delay, 0) or DEFAULT_POWER_ON_DELAY

    def set_power_off_request(self):
        """Remove a previous power on request."""
        self._power_on_requested = False

    def start_poll(self):
        """Start polling the TV for status."""
        if self._ping_thread is None or not self._ping_thread.is_alive():
            self._ping_thread = Thread(target=self._ping_thread_method)
            self._ping_thread.name = "SamsungPing"
            self._ping_thread.daemon = True
            self._ping_thread_run = True
            self._ping_thread.start()

    def stop_poll(self):
        """Stop polling the TV for status."""
        if self._ping_thread is not None and not self._ping_thread.is_alive():
            self._ping_thread_run = False
            self._ping_thread.join()
            if self._is_connected:
                self.stop_client()
            self._ping_thread = None

    def _start_client(self, *, start_all=False):
        """Start all thread that connect to the TV websocket"""

        if self._client_remote is None or not self._client_remote.is_alive():
            self._client_remote = Thread(target=self._client_remote_thread)
            self._client_remote.name = "SamsungRemote"
            self._client_remote.daemon = True
            self._client_remote.start()

            return

        if start_all:
            if self._client_control is None or not self._client_control.is_alive():
                self._client_control = Thread(target=self._client_control_thread)
                self._client_control.name = "SamsungControl"
                self._client_control.daemon = True
                self._client_control.start()

            if self._client_art_supported > 0 and (
                self._client_art is None or not self._client_art.is_alive()
            ):
                if self._client_art_supported > 1:
                    self._client_art_supported = 0
                self._client_art = Thread(target=self._client_art_thread)
                self._client_art.name = "SamsungArt"
                self._client_art.daemon = True
                self._client_art.start()

    def stop_client(self):
        """Stop the ws remote client thread."""
        if self._ws_remote:
            self._ws_remote.close()

    def open(self):
        """Open a WS client connection with the TV."""
        if self.connection is not None:
            return self.connection

        is_ssl = self._is_ssl_connection()
        url = self._format_websocket_url(_WS_ENDPOINT_REMOTE_CONTROL, is_ssl=is_ssl)
        sslopt = {"cert_reqs": ssl.CERT_NONE} if is_ssl else {}

        _LOGGING.debug("WS url %s", url)
        connection = websocket.create_connection(url, self.timeout, sslopt=sslopt)
        completed = False
        response = ""

        for _ in range(3):
            response = _process_api_response(connection.recv())
            _LOGGING.debug(response)
            event = response.get("event", "-")
            if event != "ms.channel.connect":
                break
            conn_data = response.get("data")
            if self._check_conn_id(conn_data):
                completed = True
                token = conn_data.get("token")
                if token:
                    self._set_token(token)
                break

        if not completed:
            self.close()
            raise ConnectionFailure(response)

        self.connection = connection
        return connection

    def close(self):
        """Close WS connection."""
        if self.connection:
            self.connection.close()
            _LOGGING.debug("Connection closed.")
        self.connection = None

    def send_key(self, key, key_press_delay=None, cmd="Click"):
        """Send a key to the TV using appropriate WS connection."""
        _LOGGING.debug("Sending key %s", key)
        return self._ws_send(
            {
                "method": "ms.remote.control",
                "params": {
                    "Cmd": cmd,
                    "DataOfCmd": key,
                    "Option": "false",
                    "TypeOfRemote": "SendRemoteKey",
                },
            },
            key_press_delay,
        )

    def hold_key(self, key, seconds):
        """Send a key to the TV and keep it pressed for specific number of seconds"""
        if self.send_key(key, key_press_delay=0, cmd="Press"):
            time.sleep(seconds)
            return self.send_key(key, key_press_delay=0, cmd="Release")
        return False

    def send_text(self, text, send_delay=None):
        """Send a text string to the TV."""
        if not text:
            return False

        base64_text = self._serialize_string(text)
        if self._ws_send(
            {
                "method": "ms.remote.control",
                "params": {
                    "Cmd": f"{base64_text}",
                    "DataOfCmd": "base64",
                    "TypeOfRemote": "SendInputString",
                },
            },
            key_press_delay=send_delay,
        ):
            self._ws_send(
                {
                    "method": "ms.remote.control",
                    "params": {
                        "TypeOfRemote": "SendInputEnd",
                    },
                },
                key_press_delay=0,
            )
            return True

        return False

    def move_cursor(self, x, y, duration=0):
        """Move the cursor in the TV to specific coordinate."""
        self._ws_send(
            {
                "method": "ms.remote.control",
                "params": {
                    "Cmd": "Move",
                    "Position": {"x": x, "y": y, "Time": str(duration)},
                    "TypeOfRemote": "ProcessMouseDevice",
                },
            },
            key_press_delay=0,
        )

    def run_app(self, app_id, action_type="", meta_tag="", *, use_remote=False):
        """Launch an app using appropriate WS channel."""
        if not action_type:
            app = self._installed_app.get(app_id)
            if app:
                app_type = app.app_type
            else:
                app_type = self._app_type.get(app_id, 2)
            action_type = TYPE_DEEP_LINK if app_type == 2 else TYPE_NATIVE_LAUNCH
        elif action_type != TYPE_NATIVE_LAUNCH:
            action_type = TYPE_DEEP_LINK

        _LOGGING.debug(
            "Sending run app app_id: %s app_type: %s meta_tag: %s",
            app_id,
            action_type,
            meta_tag,
        )

        if self._ws_control and action_type == TYPE_DEEP_LINK and not use_remote:
            return self._ws_send(
                {
                    "id": app_id,
                    "method": "ms.application.start",
                    "params": {"id": app_id},
                },
                key_press_delay=0,
                use_control=True,
                ws_socket=self._ws_control,
            )

        return self._ws_send(
            {
                "method": "ms.channel.emit",
                "params": {
                    "event": "ed.apps.launch",
                    "to": "host",
                    "data": {
                        # action_type: NATIVE_LAUNCH / DEEP_LINK
                        # app_type == 2 ? 'DEEP_LINK' : 'NATIVE_LAUNCH',
                        "action_type": action_type,
                        "appId": app_id,
                        "metaTag": meta_tag,
                    },
                },
            },
            key_press_delay=0,
        )

    def open_browser(self, url):
        """Launch the browser app on the TV."""
        _LOGGING.debug("Opening url in browser %s", url)
        return self.run_app("org.tizen.browser", TYPE_NATIVE_LAUNCH, url)

    def rest_device_info(self):
        """Get device info using rest api call."""
        _LOGGING.debug("Get device info via rest api")
        return self._rest_request("")

    def rest_app_status(self, app_id):
        """Get app status using rest api call."""
        _LOGGING.debug("Get app %s status via rest api", app_id)
        return self._rest_request("applications/" + app_id)

    def rest_app_run(self, app_id):
        """Run an app using rest api call."""
        _LOGGING.debug("Run app %s via rest api", app_id)
        return self._rest_request("applications/" + app_id, "POST")

    def rest_app_close(self, app_id):
        """Close an app using rest api call."""
        _LOGGING.debug("Close app %s via rest api", app_id)
        return self._rest_request("applications/" + app_id, "DELETE")

    def rest_app_install(self, app_id):
        """Install a new app using rest api call."""
        _LOGGING.debug("Install app %s via rest api", app_id)
        return self._rest_request("applications/" + app_id, "PUT")

    def shortcuts(self):
        """Return a list of available shortcuts."""
        return SamsungTVShortcuts(self)


================================================
FILE: custom_components/samsungtv_smart/api/shortcuts.py
================================================
"""
SamsungTVWS - Samsung Smart TV WS API wrapper

Copyright (C) 2019 Xchwarze

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor,
    Boston, MA  02110-1335  USA

"""


class SamsungTVShortcuts:
    def __init__(self, remote):
        self.remote = remote

    # power
    def power(self):
        self.remote.send_key("KEY_POWER")

    # menu
    def home(self):
        self.remote.send_key("KEY_HOME")

    def menu(self):
        self.remote.send_key("KEY_MENU")

    def source(self):
        self.remote.send_key("KEY_SOURCE")

    def guide(self):
        self.remote.send_key("KEY_GUIDE")

    def tools(self):
        self.remote.send_key("KEY_TOOLS")

    def info(self):
        self.remote.send_key("KEY_INFO")

    # navigation
    def up(self):
        self.remote.send_key("KEY_UP")

    def down(self):
        self.remote.send_key("KEY_DOWN")

    def left(self):
        self.remote.send_key("KEY_LEFT")

    def right(self):
        self.remote.send_key("KEY_RIGHT")

    def enter(self, count=1):
        self.remote.send_key("KEY_ENTER")

    def back(self):
        self.remote.send_key("KEY_RETURN")

    # channel
    def channel_list(self):
        self.remote.send_key("KEY_CH_LIST")

    def channel(self, ch):
        for c in str(ch):
            self.digit(c)

        self.enter()

    def digit(self, d):
        self.remote.send_key("KEY_" + d)

    def channel_up(self):
        self.remote.send_key("KEY_CHUP")

    def channel_down(self):
        self.remote.send_key("KEY_CHDOWN")

    # volume
    def volume_up(self):
        self.remote.send_key("KEY_VOLUP")

    def volume_down(self):
        self.remote.send_key("KEY_VOLDOWN")

    def mute(self):
        self.remote.send_key("KEY_MUTE")

    # extra
    def red(self):
        self.remote.send_key("KEY_RED")

    def green(self):
        self.remote.send_key("KEY_GREEN")

    def yellow(self):
        self.remote.send_key("KEY_YELLOW")

    def blue(self):
        self.remote.send_key("KEY_BLUE")


================================================
FILE: custom_components/samsungtv_smart/api/smartthings.py
================================================
"""SmartThings TV integration."""

from __future__ import annotations

from asyncio import TimeoutError as AsyncTimeoutError
from collections.abc import Callable
from datetime import timedelta
from enum import Enum
import json
import logging

from aiohttp import ClientConnectionError, ClientResponseError, ClientSession

from homeassistant.util import Throttle

API_BASEURL = "https://api.smartthings.com/v1"
API_DEVICES = f"{API_BASEURL}/devices"

DEVICE_TYPE_OCF = "OCF"
DEVICE_TYPE_NAME_TV = "Samsung OCF TV"
DEVICE_TYPE_NAMES = ["Samsung OCF TV", "x.com.st.d.monitor"]


COMMAND_POWER_OFF = {
    "capability": "switch",
    "command": "off",
}
COMMAND_POWER_ON = {
    "capability": "switch",
    "command": "on",
}
COMMAND_REFRESH = {
    "capability": "refresh",
    "command": "refresh",
}
COMMAND_SET_SOURCE = {
    "capability": "mediaInputSource",
    "command": "setInputSource",
}
COMMAND_SET_VD_SOURCE = {
    "capability": "samsungvd.mediaInputSource",
    "command": "setInputSource",
}
COMMAND_MUTE = {
    "capability": "audioMute",
    "command": "mute",
}
COMMAND_UNMUTE = {
    "capability": "audioMute",
    "command": "unmute",
}
COMMAND_VOLUME_UP = {
    "capability": "audioVolume",
    "command": "volumeUp",
}
COMMAND_VOLUME_DOWN = {
    "capability": "audioVolume",
    "command": "volumeDown",
}
COMMAND_SET_VOLUME = {
    "capability": "audioVolume",
    "command": "setVolume",
}
COMMAND_CHANNEL_UP = {
    "capability": "tvChannel",
    "command": "channelUp",
}
COMMAND_CHANNEL_DOWN = {
    "capability": "tvChannel",
    "command": "channelDown",
}
COMMAND_SET_CHANNEL = {
    "capability": "tvChannel",
    "command": "setTvChannel",
}
COMMAND_PAUSE = {
    "capability": "mediaPlayback",
    "command": "pause",
}
COMMAND_PLAY = {
    "capability": "mediaPlayback",
    "command": "play",
}
COMMAND_STOP = {
    "capability": "mediaPlayback",
    "command": "stop",
}
COMMAND_FAST_FORWARD = {
    "capability": "mediaPlayback",
    "command": "fastForward",
}
COMMAND_REWIND = {
    "capability": "mediaPlayback",
    "command": "rewind",
}
COMMAND_SOUND_MODE = {
    "capability": "custom.soundmode",
    "command": "setSoundMode",
}
COMMAND_PICTURE_MODE = {
    "capability": "custom.picturemode",
    "command": "setPictureMode",
}

DIGITAL_TV = "digitalTv"

MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
_LOGGER = logging.getLogger(__name__)


def _headers(api_key: str) -> dict[str, str]:
    return {
        "Authorization": f"Bearer {api_key}",
        "Accept": "application/json",
        "Connection": "keep-alive",
    }


def _command(command: dict, arguments: list | None = None):
    cmd = {"component": "main", **command}
    if arguments:
        cmd["arguments"] = arguments
    cmd_full = {"commands": [cmd]}
    return str(cmd_full)


class STStatus(Enum):
    """Represent SmartThings status."""

    STATE_OFF = 0
    STATE_ON = 1
    STATE_UNKNOWN = 2


class SmartThingsTV:
    """Class to read status for TV registered in SmartThings cloud."""

    def __init__(
        self,
        api_key: str,
        device_id: str,
        use_channel_info: bool = True,
        session: ClientSession | None = None,
        api_key_callback: Callable[[], str | None] | None = None,
    ):
        """Initialize SmartThingsTV."""
        self._api_key = api_key
        self._device_id = device_id
        self._use_channel_info = use_channel_info
        if session:
            self._session = session
            self._managed_session = False
        else:
            self._session = ClientSession()
            self._managed_session = True

        self._device_name = None
        self._state = STStatus.STATE_UNKNOWN
        self._prev_state = STStatus.STATE_UNKNOWN
        self._muted = False
        self._volume = 10
        self._source_list = None
        self._source_list_map = None
        self._source = ""
        self._channel = ""
        self._channel_name = ""
        self._sound_mode = None
        self._sound_mode_list = None
        self._picture_mode = None
        self._picture_mode_list = None

        self._is_forced_val = False
        self._forced_count = 0

        self._api_key_callback = api_key_callback

    def __enter__(self):
        return self

    def __exit__(self, ext_type, ext_value, ext_traceback):
        pass

    def _get_api_key(self) -> str:
        """Get API key used to connect to smartthink."""
        if self._api_key_callback is not None:
            if api_key := self._api_key_callback():
                self._api_key = api_key
        return self._api_key

    @property
    def api_key(self) -> str:
        """Return current api_key."""
        return self._api_key

    @property
    def device_id(self) -> str:
        """Return current device_id."""
        return self._device_id

    @property
    def device_name(self) -> str:
        """Return current device_name."""
        return self._device_name

    @property
    def state(self):
        """Return current state."""
        return self._state

    @property
    def prev_state(self):
        """Return current state."""
        return self._prev_state

    @property
    def muted(self) -> bool:
        """Return current muted state."""
        return self._muted

    @property
    def volume(self) -> int:
        """Return current volume."""
        return self._volume

    @property
    def source(self) -> str:
        """Return current source."""
        return self._source

    @property
    def channel(self) -> str:
        """Return current channel."""
        return self._channel

    @property
    def channel_name(self) -> str:
        """Return current channel name."""
        return self._channel_name

    @property
    def source_list(self):
        """Return available source list."""
        return self._source_list

    @property
    def sound_mode(self):
        """Return current sound mode."""
        if self._state != STStatus.STATE_ON:
            return None
        return self._sound_mode

    @property
    def sound_mode_list(self):
        """Return available sound modes."""
        if self._state != STStatus.STATE_ON:
            return None
        return self._sound_mode_list

    @property
    def picture_mode(self):
        """Return current picture mode."""
        if self._state != STStatus.STATE_ON:
            return None
        return self._picture_mode

    @property
    def picture_mode_list(self):
        """Return available picture modes."""
        if self._state != STStatus.STATE_ON:
            return None
        return self._picture_mode_list

    def get_source_name(self, source_id: str) -> str:
        """Get source name based on source id."""
        if not self._source_list_map:
            return ""
        if source_id.upper() == DIGITAL_TV.upper():
            source_id = "dtv"
        for map_value in self._source_list_map:
            map_id = map_value.get("id")
            if map_id and map_id == source_id:
                return map_value.get("name", "")
        return ""

    def _get_source_list_from_map(self) -> list:
        """Return source list from source map."""
        if not self._source_list_map:
            return []
        source_list = []
        for map_value in self._source_list_map:
            if source_id := map_value.get("id"):
                if source_id.upper() == "DTV":
                    source_list.append(DIGITAL_TV)
                else:
                    source_list.append(source_id)
        return source_list

    def set_application(self, app_id):
        """Set running application info."""
        if self._use_channel_info:
            self._channel = ""
            self._channel_name = app_id
            self._is_forced_val = True
            self._forced_count = 0

    def _set_source(self, source):
        """Set current source info."""
        if source != self._source:
            self._source = source
            self._channel = ""
            self._channel_name = ""
            self._is_forced_val = True
            self._forced_count = 0

    @staticmethod
    def _load_json_list(dev_data, list_name):
        """Try load a list from string to json format."""
        load_list = []
        json_list = dev_data.get(list_name, {}).get("value")
        if json_list:
            try:
                load_list = json.loads(json_list)
            except (TypeError, ValueError):
                pass
        return load_list

    @staticmethod
    async def get_devices_list(api_key, session: ClientSession, device_label=""):
        """Get list of available SmartThings devices"""

        result = {}

        async with session.get(
            API_DEVICES,
            headers=_headers(api_key),
            raise_for_status=True,
        ) as resp:
            device_list = await resp.json()

        if device_list:
            _LOGGER.debug("SmartThings available devices: %s", str(device_list))

            for dev in device_list.get("items", []):
                if (device_id := dev.get("deviceId")) is None:
                    continue
                if dev.get("type", "") != DEVICE_TYPE_OCF:
                    continue

                label = dev.get("label", "")
                if device_label:
                    if label != device_label:
                        continue
                elif dev.get("deviceTypeName", "") not in DEVICE_TYPE_NAMES:
                    continue

                result[device_id] = {
                    "name": dev.get("name", f"TV ID {device_id}"),
                    "label": label,
                }

        _LOGGER.info("SmartThings discovered TV devices: %s", str(result))

        return result

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    async def _device_refresh(self, **kwargs):
        """Refresh device status on SmartThings"""

        device_id = self._device_id
        if not device_id:
            return

        api_device = f"{API_DEVICES}/{device_id}"
        api_command = f"{api_device}/commands"

        if self._use_channel_info:
            async with self._session.post(
                api_command,
                headers=_headers(self._get_api_key()),
                data=_command(COMMAND_REFRESH),
                raise_for_status=False,
            ) as resp:
                if resp.status == 409:
                    self._state = STStatus.STATE_OFF
                    return
                resp.raise_for_status()
                await resp.json()

        return

    async def _async_send_command(self, data_cmd):
        """Send a command via SmartThings"""
        device_id = self._device_id
        if not device_id:
            return
        if not data_cmd:
            return

        api_device = f"{API_DEVICES}/{device_id}"
        api_command = f"{api_device}/commands"

        async with self._session.post(
            api_command,
            headers=_headers(self._get_api_key()),
            data=data_cmd,
            raise_for_status=True,
        ) as resp:
            await resp.json()

        await self._device_refresh()

    async def async_device_health(self):
        """Check device availability"""

        device_id = self._device_id
        if not device_id:
            return False

        api_device = f"{API_DEVICES}/{device_id}"
        api_device_health = f"{api_device}/health"

        # this get the real status of the device
        async with self._session.get(
            api_device_health,
            headers=_headers(self._get_api_key()),
            raise_for_status=True,
        ) as resp:
            health = await resp.json()

        _LOGGER.debug(health)

        if health["state"] == "ONLINE":
            return True
        return False

    async def async_device_update(self, use_channel_info: bool = None):
        """Query device status on SmartThing"""

        device_id = self._device_id
        if not device_id:
            return

        if use_channel_info is not None:
            self._use_channel_info = use_channel_info

        api_device = f"{API_DEVICES}/{device_id}"
        api_device_status = f"{api_device}/states"
        # not used, just for reference
        # api_device_main_status = f"{api_device}/components/main/status"

        self._prev_state = self._state

        try:
            is_online = await self.async_device_health()
        except (
            AsyncTimeoutError,
            ClientConnectionError,
            ClientResponseError,
        ):
            self._state = STStatus.STATE_UNKNOWN
            return

        if is_online:
            self._state = STStatus.STATE_ON
        else:
            self._state = STStatus.STATE_OFF
            return

        await self._device_refresh()
        if self._state == STStatus.STATE_OFF:
            return

        async with self._session.get(
            api_device_status,
            headers=_headers(self._get_api_key()),
            raise_for_status=True,
        ) as resp:
            data = await resp.json()

        _LOGGER.debug(data)

        dev_data = data.get("main", {})
        # device_state = data['main']['switch']['value']

        # Volume
        device_volume = dev_data.get("volume", {}).get("value", 0)
        if device_volume and device_volume.isdigit():
            self._volume = int(device_volume) / 100
        else:
            self._volume = 0

        # Muted state
        device_muted = dev_data.get("mute", {}).get("value", "")
        self._muted = device_muted == "mute"

        # Sound Mode
        self._sound_mode = dev_data.get("soundMode", {}).get("value")
        self._sound_mode_list = self._load_json_list(dev_data, "supportedSoundModes")

        # Picture Mode
        self._picture_mode = dev_data.get("pictureMode", {}).get("value")
        self._picture_mode_list = self._load_json_list(
            dev_data, "supportedPictureModes"
        )

        # Sources and channel
        self._source_list_map = self._load_json_list(
            dev_data, "supportedInputSourcesMap"
        )
        # self._source_list = self._load_json_list(dev_data, "supportedInputSources")
        self._source_list = self._get_source_list_from_map()

        if self._is_forced_val and self._forced_count <= 0:
            self._forced_count += 1
            return
        self._is_forced_val = False
        self._forced_count = 0

        device_source = dev_data.get("inputSource", {}).get("value", "")
        device_tv_chan = dev_data.get("tvChannel", {}).get("value", "")
        device_tv_chan_name = dev_data.get("tvChannelName", {}).get("value", "")

        if device_source:
            if device_source.upper() == DIGITAL_TV.upper():
                device_source = DIGITAL_TV
        self._source = device_source
        # if the status is not refreshed this info may become not reliable
        if self._use_channel_info:
            self._channel = device_tv_chan
            self._channel_name = device_tv_chan_name
        else:
            self._channel = ""
            self._channel_name = ""

    async def async_turn_off(self):
        """Turn off TV via SmartThings"""
        data_cmd = _command(COMMAND_POWER_OFF)
        await self._async_send_command(data_cmd)

    async def async_turn_on(self):
        """Turn on TV via SmartThings"""
        data_cmd = _command(COMMAND_POWER_ON)
        await self._async_send_command(data_cmd)

    async def async_send_command(self, cmd_type, command=""):
        """Send a command to the device"""
        data_cmd = None

        if cmd_type == "setvolume":  # sets volume
            data_cmd = _command(COMMAND_SET_VOLUME, [int(command)])
        elif cmd_type == "stepvolume":  # steps volume up or down
            if command == "up":
                data_cmd = _command(COMMAND_VOLUME_UP)
            elif command == "down":
                data_cmd = _command(COMMAND_VOLUME_DOWN)
        elif cmd_type == "audiomute":  # mutes audio
            if command == "on":
                data_cmd = _command(COMMAND_MUTE)
            elif command == "off":
                data_cmd = _command(COMMAND_UNMUTE)
        elif cmd_type == "selectchannel":  # changes channel
            data_cmd = _command(COMMAND_SET_CHANNEL, [command])
        elif cmd_type == "stepchannel":  # steps channel up or down
            if command == "up":
                data_cmd = _command(COMMAND_CHANNEL_UP)
            elif command == "down":
                data_cmd = _command(COMMAND_CHANNEL_DOWN)
        else:
            return

        await self._async_send_command(data_cmd)

    async def async_select_source(self, source):
        """Select source"""
        # if source not in self._source_list:
        #     return
        data_cmd = _command(COMMAND_SET_SOURCE, [source])
        # set property to reflect new changes
        self._set_source(source)
        await self._async_send_command(data_cmd)

    async def async_select_vd_source(self, source):
        """Select source"""
        # if source not in self._source_list:
        #     return
        data_cmd = _command(COMMAND_SET_VD_SOURCE, [source])
        await self._async_send_command(data_cmd)

    async def async_set_sound_mode(self, mode):
        """Select sound mode"""
        if self._state != STStatus.STATE_ON:
            return
        if mode not in self._sound_mode_list:
            raise InvalidSmartThingsSoundMode()
        data_cmd = _command(COMMAND_SOUND_MODE, [mode])
        await self._async_send_command(data_cmd)
        self._sound_mode = mode

    async def async_set_picture_mode(self, mode):
        """Select picture mode"""
        if self._state != STStatus.STATE_ON:
            return
        if mode not in self._picture_mode_list:
            raise InvalidSmartThingsPictureMode()
        data_cmd = _command(COMMAND_PICTURE_MODE, [mode])
        await self._async_send_command(data_cmd)
        self._picture_mode = mode


class InvalidSmartThingsSoundMode(RuntimeError):
    """Selected sound mode is invalid."""


class InvalidSmartThingsPictureMode(RuntimeError):
    """Selected picture mode is invalid."""


================================================
FILE: custom_components/samsungtv_smart/api/upnp.py
================================================
"""Smartthings TV integration UPnP implementation."""

import logging
from typing import Optional
import xml.etree.ElementTree as ET

from aiohttp import ClientSession
import async_timeout

DEFAULT_TIMEOUT = 0.2

_LOGGER = logging.getLogger(__name__)


class SamsungUPnP:
    """UPnP implementation for Samsung TV."""

    def __init__(self, host, session: Optional[ClientSession] = None):
        """Initialize the class."""
        self._host = host
        self._connected = False
        if session:
            self._session = session
            self._managed_session = False
        else:
            self._session = ClientSession()
            self._managed_session = True

    async def _soap_request(
        self, action, arguments, protocole, *, timeout=DEFAULT_TIMEOUT
    ):
        """Send a SOAP request to the TV."""
        headers = {
            "SOAPAction": f'"urn:schemas-upnp-org:service:{protocole}:1#{action}"',
            "content-type": "text/xml",
        }
        body = f"""<?xml version="1.0" encoding="utf-8"?>
                <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
                    <s:Body>
                    <u:{action} xmlns:u="urn:schemas-upnp-org:service:{protocole}:1">
                        <InstanceID>0</InstanceID>
                        {arguments}
                    </u:{action}>
                    </s:Body>
                </s:Envelope>"""
        try:
            async with async_timeout.timeout(timeout):
                async with self._session.post(
                    f"http://{self._host}:9197/upnp/control/{protocole}1",
                    headers=headers,
                    data=body,
                    raise_for_status=True,
                ) as resp:
                    response = await resp.content.read()
                    self._connected = True
        except Exception as exc:  # pylint: disable=broad-except
            _LOGGER.debug(exc)
            self._connected = False
            return None

        return response

    @property
    def connected(self):
        """Return if connected to Samsung TV."""
        return self._connected

    async def async_disconnect(self):
        """Disconnect from TV and close session."""
        if self._managed_session:
            await self._session.close()

    async def async_get_volume(self):
        """Return volume status."""
        response = await self._soap_request(
            "GetVolume", "<Channel>Master</Channel>", "RenderingControl"
        )
        if response is None:
            return None

        tree = ET.fromstring(response.decode("utf8"))
        volume = None
        for elem in tree.iter(tag="CurrentVolume"):
            volume = elem.text
        return volume

    async def async_set_volume(self, volume):
        """Set the volume level."""
        await self._soap_request(
            "SetVolume",
            f"<Channel>Master</Channel><DesiredVolume>{volume}</DesiredVolume>",
            "RenderingControl",
        )

    async def async_get_mute(self):
        """Return mute status."""
        response = await self._soap_request(
            "GetMute", "<Channel>Master</Channel>", "RenderingControl"
        )
        if response is None:
            return None

        tree = ET.fromstring(response.decode("utf8"))
        mute = None
        for elem in tree.iter(tag="CurrentMute"):
            mute = elem.text
        if mute is None:
            return None
        return int(mute) != 0

    async def async_set_current_media(self, url):
        """Set media to playback and play it."""

        if (
            await self._soap_request(
                "SetAVTransportURI",
                f"<CurrentURI>{url}</CurrentURI><CurrentURIMetaData></CurrentURIMetaData>",
                "AVTransport",
                timeout=2.0,
            )
            is None
        ):
            return False

        await self._soap_request("Play", "<Speed>1</Speed>", "AVTransport")
        return True

    async def async_play(self):
        """Play media that was already set as current."""
        await self._soap_request("Play", "<Speed>1</Speed>", "AVTransport")


================================================
FILE: custom_components/samsungtv_smart/config_flow.py
================================================
"""Config flow for Samsung TV."""

from __future__ import annotations

import logging
from numbers import Number
import socket
from typing import Any, Dict

import voluptuous as vol

from homeassistant.components.binary_sensor import DOMAIN as BS_DOMAIN
from homeassistant.config_entries import (
    SOURCE_RECONFIGURE,
    SOURCE_USER,
    ConfigEntry,
    ConfigFlow,
    ConfigFlowResult,
    OptionsFlow,
)
from homeassistant.const import (
    ATTR_DEVICE_ID,
    CONF_API_KEY,
    CONF_BASE,
    CONF_DEVICE_ID,
    CONF_HOST,
    CONF_ID,
    CONF_MAC,
    CONF_NAME,
    CONF_PORT,
    CONF_TOKEN,
    SERVICE_TURN_ON,
    __version__,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
    EntitySelector,
    EntitySelectorConfig,
    ObjectSelector,
    SelectOptionDict,
    SelectSelector,
    SelectSelectorConfig,
    SelectSelectorMode,
)

from . import (
    SamsungTVInfo,
    get_device_info,
    get_smartthings_api_key,
    get_smartthings_entries,
    is_valid_ha_version,
)
from .const import (
    ATTR_DEVICE_MAC,
    ATTR_DEVICE_MODEL,
    ATTR_DEVICE_NAME,
    ATTR_DEVICE_OS,
    CONF_APP_LAUNCH_METHOD,
    CONF_APP_LIST,
    CONF_APP_LOAD_METHOD,
    CONF_CHANNEL_LIST,
    CONF_DEVICE_MODEL,
    CONF_DEVICE_NAME,
    CONF_DEVICE_OS,
    CONF_DUMP_APPS,
    CONF_EXT_POWER_ENTITY,
    CONF_LOGO_OPTION,
    CONF_PING_PORT,
    CONF_POWER_ON_METHOD,
    CONF_SHOW_CHANNEL_NR,
    CONF_SOURCE_LIST,
    CONF_ST_ENTRY_UNIQUE_ID,
    CONF_SYNC_TURN_OFF,
    CONF_SYNC_TURN_ON,
    CONF_TOGGLE_ART_MODE,
    CONF_USE_LOCAL_LOGO,
    CONF_USE_MUTE_CHECK,
    CONF_USE_ST_CHANNEL_INFO,
    CONF_USE_ST_STATUS_INFO,
    CONF_WOL_REPEAT,
    CONF_WS_NAME,
    DOMAIN,
    MAX_WOL_REPEAT,
    RESULT_ST_DEVICE_NOT_FOUND,
    RESULT_ST_DEVICE_USED,
    RESULT_SUCCESS,
    RESULT_WRONG_APIKEY,
    AppLaunchMethod,
    AppLoadMethod,
    PowerOnMethod,
    __min_ha_version__,
)
from .logo import LOGO_OPTION_DEFAULT, LogoOption

APP_LAUNCH_METHODS = {
    AppLaunchMethod.Standard.value: "Control Web Socket Channel",
    AppLaunchMethod.Remote.value: "Remote Web Socket Channel",
    AppLaunchMethod.Rest.value: "Rest API Call",
}

APP_LOAD_METHODS = {
    AppLoadMethod.All.value: "All Apps",
    AppLoadMethod.Default.value: "Default Apps",
    AppLoadMethod.NotLoad.value: "Not Load",
}

LOGO_OPTIONS = {
    LogoOption.Disabled.value: "Disabled",
    LogoOption.WhiteColor.value: "White background, Color logo",
    LogoOption.BlueColor.value: "Blue background, Color logo",
    LogoOption.BlueWhite.value: "Blue background, White logo",
    LogoOption.DarkWhite.value: "Dark background, White logo",
    LogoOption.TransparentColor.value: "Transparent background, Color logo",
    LogoOption.TransparentWhite.value: "Transparent background, White logo",
}

POWER_ON_METHODS = {
    PowerOnMethod.WOL.value: "WOL Packet (better for wired connection)",
    PowerOnMethod.SmartThings.value: "SmartThings (better for wireless connection)",
}

CONF_SHOW_ADV_OPT = "show_adv_opt"
CONF_ST_DEVICE = "st_devices"
CONF_USE_HA_NAME = "use_ha_name_for_ws"

ADVANCED_OPTIONS = [
    CONF_APP_LAUNCH_METHOD,
    CONF_DUMP_APPS,
    CONF_EXT_POWER_ENTITY,
    CONF_PING_PORT,
    CONF_WOL_REPEAT,
    CONF_TOGGLE_ART_MODE,
    CONF_USE_MUTE_CHECK,
]

ENUM_OPTIONS = [
    CONF_APP_LOAD_METHOD,
    CONF_APP_LAUNCH_METHOD,
    CONF_LOGO_OPTION,
    CONF_POWER_ON_METHOD,
]

_LOGGER = logging.getLogger(__name__)


def _get_ip(host):
    if host is None:
        return None
    try:
        return socket.gethostbyname(host)
    except socket.gaierror:
        return None


class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
    """Handle a Samsung TV config flow."""

    VERSION = 1

    # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167

    def __init__(self) -> None:
        """Initialize flow."""
        self._user_data = None
        self._st_devices_schema = None

        self._tv_info: SamsungTVInfo | None = None
        self._host = None
        self._api_key = None
        self._st_entry_unique_id = None
        self._device_id = None
        self._name = None
        self._ws_name = None
        self._logo_option = None
        self._device_info = {}
        self._token = None
        self._ping_port = None
        self._error: str | None = None

    def _stdev_already_used(self, devices_id) -> bool:
        """Check if a device_id is in HA config."""
        for entry in self._async_current_entries():
            if entry.data.get(CONF_DEVICE_ID, "") == devices_id:
                return True
        return False

    def _remove_stdev_used(self, devices_list: Dict[str, Any]) -> Dict[str, Any]:
        """Remove entry already used"""
        res_dev_list = devices_list.copy()

        for dev_id in devices_list.keys():
            if self._stdev_already_used(dev_id):
                res_dev_list.pop(dev_id)
        return res_dev_list

    @staticmethod
    def _extract_dev_name(device) -> str:
        """Extract device neme from SmartThings Info"""
        name = device["name"]
        label = device.get("label", "")
        if label:
            name += f" ({label})"
        return name

    def _prepare_dev_schema(self, devices_list) -> vol.Schema:
        """Prepare the schema for select correct ST device"""
        validate = {}
        for dev_id, infos in devices_list.items():
            device_name = self._extract_dev_name(infos)
            validate[dev_id] = device_name
        return vol.Schema({vol.Required(CONF_ST_DEVICE): vol.In(validate)})

    async def _get_st_deviceid(self, st_device_label="") -> str:
        """Try to detect SmartThings device id."""
        session = async_get_clientsession(self.hass)
        devices_list = await SamsungTVInfo.get_st_devices(
            self._api_key, session, st_device_label
        )
        if devices_list is None:
            return RESULT_WRONG_APIKEY

        devices_list = self._remove_stdev_used(devices_list)
        if devices_list:
            if len(devices_list) > 1:
                self._st_devices_schema = self._prepare_dev_schema(devices_list)
            else:
                self._device_id = list(devices_list.keys())[0]

        return RESULT_SUCCESS

    async def _try_connect(self, *, port=None, token=None, skip_info=False) -> str:
        """Try to connect and check auth."""
        self._tv_info = SamsungTVInfo(self.hass, self._host, self._ws_name)

        session = async_get_clientsession(self.hass)
        result = await self._tv_info.try_connect(
            session, self._api_key, self._device_id, ws_port=port, ws_token=token
        )
        if result == RESULT_SUCCESS:
            self._token = self._tv_info.ws_token
            self._ping_port = self._tv_info.ping_port
            if not skip_info:
                self._device_info = await get_device_info(self._host, session)

        return result

    @callback
    def _get_api_key(self) -> str | None:
        """Get api key in configured entries if available."""
        for entry in self._async_current_entries():
            if CONF_API_KEY in entry.data:
                if not entry.data.get(CONF_ST_ENTRY_UNIQUE_ID):
                    return entry.data[CONF_API_KEY]
        return None

    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle a flow initialized by the user."""

        if not is_valid_ha_version():
            return self.async_abort(
                reason="unsupported_version",
                description_placeholders={
                    "req_ver": __min_ha_version__,
                    "run_ver": __version__,
                },
            )

        if not self._user_data:
            if api_key := self._get_api_key():
                self._user_data = {CONF_API_KEY: api_key}

        if user_input is None:
            return self._show_form()

        self._user_data = user_input
        ip_address = await self.hass.async_add_executor_job(
            _get_ip, user_input[CONF_HOST]
        )
        if not ip_address:
            return self._show_form(errors="invalid_host")

        self._async_abort_entries_match({CONF_HOST: ip_address})

        self._host = ip_address
        self._name = user_input[CONF_NAME]
        api_key = user_input.get(CONF_API_KEY)
        st_entry_unique_id = user_input.get(CONF_ST_ENTRY_UNIQUE_ID)
        if api_key and st_entry_unique_id:
            return self._show_form(errors="only_key_or_st")

        self._st_entry_unique_id = None
        if st_entry_unique_id:
            if not (api_key := get_smartthings_api_key(self.hass, st_entry_unique_id)):
                return self._show_form(errors="st_api_key_fail")
            self._st_entry_unique_id = st_entry_unique_id

        self._api_key = api_key

        use_ha_name = user_input.get(CONF_USE_HA_NAME, False)
        if use_ha_name:
            ha_conf = self.hass.config
            if hasattr(ha_conf, "location_name"):
                self._ws_name = ha_conf.location_name
        if not self._ws_name:
            self._ws_name = self._name

        result = RESULT_SUCCESS
        if self._api_key:
            result = await self._get_st_deviceid()

            if result == RESULT_SUCCESS and not self._device_id:
                if self._st_devices_schema:
                    return await self.async_step_stdevice()
                return await self.async_step_stdeviceid()

        if result == RESULT_SUCCESS:
            result = await self._try_connect()

        return await self._manage_result(result, True)

    async def async_step_stdevice(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle a flow to select ST device."""
        if user_input is None:
            return self._show_form(step_id="stdevice")

        self._device_id = user_input.get(CONF_ST_DEVICE)

        result = await self._try_connect()
        return await self._manage_result(result)

    async def async_step_stdeviceid(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle a flow to manual input a ST device."""
        if user_input is None:
            return self._show_form(step_id="stdeviceid")

        device_id = user_input.get(CONF_DEVICE_ID)
        if self._stdev_already_used(device_id):
            return self._show_form(errors=RESULT_ST_DEVICE_USED, step_id="stdeviceid")

        self._device_id = device_id

        result = await self._try_connect()
        if result == RESULT_ST_DEVICE_NOT_FOUND:
            return self._show_form(errors=result, step_id="stdeviceid")
        return await self._manage_result(result)

    async def async_step_reconfigure(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle reconfiguration of the integration."""
        entry = self._get_reconfigure_entry()
        if entry.unique_id == entry.data[CONF_HOST]:
            return self.async_abort(reason="host_unique_id")

        if not self._ws_name:
            self._ws_name = entry.data[CONF_WS_NAME]
            if CONF_API_KEY in entry.data:
                self._device_id = entry.data.get(CONF_DEVICE_ID)

        if user_input is None:
            return self._show_form(errors=None, step_id=SOURCE_RECONFIGURE)

        ip_address = await self.hass.async_add_executor_job(
            _get_ip, user_input[CONF_HOST]
        )
        if not ip_address:
            return self._show_form(errors="invalid_host", step_id=SOURCE_RECONFIGURE)

        self._async_abort_entries_match({CONF_HOST: ip_address})

        api_key = user_input.get(CONF_API_KEY)
        st_entry_unique_id = user_input.get(CONF_ST_ENTRY_UNIQUE_ID)
        if api_key and st_entry_unique_id:
            return self._show_form(errors="only_key_or_st", step_id=SOURCE_RECONFIGURE)

        self._st_entry_unique_id = None
        if st_entry_unique_id:
            if not (api_key := get_smartthings_api_key(self.hass, st_entry_unique_id)):
                return self._show_form(
                    errors="st_api_key_fail", step_id=SOURCE_RECONFIGURE
                )
            self._st_entry_unique_id = st_entry_unique_id
        else:
            api_key = api_key or entry.data.get(CONF_API_KEY)

        self._host = ip_address
        self._api_key = api_key

        result = await self._try_connect(
            port=entry.data.get(CONF_PORT),
            token=entry.data.get(CONF_TOKEN),
            skip_info=True,
        )
        return self._manage_reconfigure(result)

    async def _manage_result(self, result: str, is_user_step=False) -> ConfigFlowResult:
        """Manage the previous result."""

        if result != RESULT_SUCCESS:
            self._error = result
            if result == RESULT_ST_DEVICE_NOT_FOUND:
                return await self.async_step_stdeviceid()
            if is_user_step:
                return self._show_form()
            return await self.async_step_user()

        if ATTR_DEVICE_ID in self._device_info:
            unique_id = self._device_info[ATTR_DEVICE_ID]
        else:
            mac = self._device_info.get(ATTR_DEVICE_MAC)
            unique_id = mac or self._host  # as last option we use host as unique id

        await self.async_set_unique_id(unique_id)
        self._abort_if_unique_id_configured()

        return self._save_entry()

    @callback
    def _manage_reconfigure(self, result: str) -> ConfigFlowResult:
        """Manage the reconfigure result."""

        if result != RESULT_SUCCESS:
            self._error = result
            return self._show_form(step_id=SOURCE_RECONFIGURE)

        entry = self._get_reconfigure_entry()
        updates = {
            CONF_HOST: self._host,
            CONF_PORT: self._tv_info.ws_port,
        }
        if self._token:
            updates[CONF_TOKEN] = self._token

        if self._api_key:
            updates[CONF_API_KEY] = self._api_key
            if CONF_ST_ENTRY_UNIQUE_ID in entry.data or self._st_entry_unique_id:
                updates[CONF_ST_ENTRY_UNIQUE_ID] = self._st_entry_unique_id

        return self.async_update_reload_and_abort(
            entry, data_updates=updates, reload_even_if_entry_is_unchanged=False
        )

    @callback
    def _save_entry(self) -> ConfigFlowResult:
        """Generate new entry."""
        data = {
            CONF_HOST: self._host,
            CONF_NAME: self._name,
            CONF_PORT: self._tv_info.ws_port,
            CONF_WS_NAME: self._ws_name,
        }
        if self._token:
            data[CONF_TOKEN] = self._token

        for key, attr in {
            CONF_ID: ATTR_DEVICE_ID,
            CONF_DEVICE_NAME: ATTR_DEVICE_NAME,
            CONF_DEVICE_MODEL: ATTR_DEVICE_MODEL,
            CONF_DEVICE_OS: ATTR_DEVICE_OS,
            CONF_MAC: ATTR_DEVICE_MAC,
        }.items():
            if attr in self._device_info:
                data[key] = self._device_info[attr]

        title = self._name
        if self._api_key and self._device_id:
            data[CONF_API_KEY] = self._api_key
            data[CONF_DEVICE_ID] = self._device_id
            if self._st_entry_unique_id:
                data[CONF_ST_ENTRY_UNIQUE_ID] = self._st_entry_unique_id
            title += " (SmartThings)"

        options = None
        if self._ping_port:
            options = {CONF_PING_PORT: self._ping_port}

        _LOGGER.info("Configured new entity %s with host %s", title, self._host)
        return self.async_create_entry(title=title, data=data, options=options)

    def _get_init_schema(self) -> vol.Schema:
        """Return the schema for initial configuration form."""
        data = self._user_data or {}
        st_entries = get_smartthings_entries(self.hass)

        init_schema = {
            vol.Required(CONF_HOST, default=data.get(CONF_HOST, "")): str,
            vol.Required(CONF_NAME, default=data.get(CONF_NAME, "")): str,
            vol.Optional(
                CONF_USE_HA_NAME, default=data.get(CONF_USE_HA_NAME, False)
            ): bool,
            vol.Optional(
                CONF_API_KEY,
                description={"suggested_value": data.get(CONF_API_KEY, "")},
            ): str,
        }

        if st_entries:
            st_unique_id = data.get(CONF_ST_ENTRY_UNIQUE_ID)
            sugg_val = st_unique_id if st_unique_id in st_entries else None
            init_schema.update(
                {
                    vol.Optional(
                        CONF_ST_ENTRY_UNIQUE_ID,
                        description={"suggested_value": sugg_val},
                    ): SelectSelector(_dict_to_select(st_entries)),
                }
            )

        return vol.Schema(init_schema)

    def _get_reconfigure_schema(self) -> vol.Schema:
        """Return the schema for reconfiguration form."""
        entry = self._get_reconfigure_entry()
        data = entry.data
        st_entries = get_smartthings_entries(self.hass)

        init_schema = {
            vol.Required(CONF_HOST, default=data.get(CONF_HOST, "")): str,
        }

        if CONF_API_KEY in data and CONF_DEVICE_ID in data:
            st_unique_id = data.get(CONF_ST_ENTRY_UNIQUE_ID)
            use_st_key = st_entries is not None and st_unique_id in st_entries
            sugg_val = data[CONF_API_KEY] if not use_st_key else ""
            init_schema.update(
                {
                    vol.Optional(
                        CONF_API_KEY, description={"suggested_value": sugg_val}
                    ): str,
                }
            )

            if st_entries:
                sugg_val = st_unique_id if use_st_key else None
                init_schema.update(
                    {
                        vol.Optional(
                            CONF_ST_ENTRY_UNIQUE_ID,
                            description={"suggested_value": sugg_val},
                        ): SelectSelector(_dict_to_select(st_entries)),
                    }
                )

        return vol.Schema(init_schema)

    @callback
    def _show_form(
        self, errors: str | None = None, step_id=SOURCE_USER
    ) -> ConfigFlowResult:
        """Show the form to the user."""
        base_err = errors or self._error
        self._error = None

        if step_id == "stdevice":
            data_schema = self._st_devices_schema
        elif step_id == "stdeviceid":
            data_schema = vol.Schema({vol.Required(CONF_DEVICE_ID): str})
        elif step_id == "reconfigure":
            data_schema = self._get_reconfigure_schema()
        else:
            data_schema = self._get_init_schema()

        return self.async_show_form(
            step_id=step_id,
            data_schema=data_schema,
            errors={CONF_BASE: base_err} if base_err else None,
        )

    @staticmethod
    @callback
    def async_get_options_flow(config_entry) -> OptionsFlowHandler:
        """Get the options flow for this handler."""
        return OptionsFlowHandler(config_entry)


class OptionsFlowHandler(OptionsFlow):
    """Handle an option flow for Samsung TV Smart."""

    def __init__(self, config_entry: ConfigEntry) -> None:
        """Initialize options flow."""
        self._entry_id = config_entry.entry_id
        self._adv_chk = False
        self._std_options = config_entry.options.copy()
        self._adv_options = {
            key: values
            for key, values in config_entry.options.items()
            if key in ADVANCED_OPTIONS
        }
        self._sync_ent_opt = {
            key: values
            for key, values in config_entry.options.items()
            if key in [CONF_SYNC_TURN_OFF, CONF_SYNC_TURN_ON]
        }
        self._app_list = self._std_options.get(CONF_APP_LIST)
        self._channel_list = self._std_options.get(CONF_CHANNEL_LIST)
        self._source_list = self._std_options.get(CONF_SOURCE_LIST)
        api_key = config_entry.data.get(CONF_API_KEY)
        st_dev = config_entry.data.get(CONF_DEVICE_ID)
        self._use_st = api_key and st_dev

    @callback
    def _save_entry(self, data) -> ConfigFlowResult:
        """Save configuration options"""
        data.update(self._adv_options)
        data.update(self._sync_ent_opt)
        entry_data = {k: v for k, v in data.items() if v is not None}
        for key, value in entry_data.items():
            if key in ENUM_OPTIONS:
                entry_data[key] = int(value)
        entry_data[CONF_APP_LIST] = self._app_list or {}
        entry_data[CONF_CHANNEL_LIST] = self._channel_list or {}
        entry_data[CONF_SOURCE_LIST] = self._source_list or {}

        return self.async_create_entry(title="", data=entry_data)

    async def async_step_init(self, user_input=None) -> ConfigFlowResult:
        """Handle initial options flow."""
        if user_input is not None:
            if self._adv_chk or user_input.pop(CONF_SHOW_ADV_OPT, False):
                self._adv_chk = True
                self._std_options = user_input
                return await self.async_step_menu()
            return self._save_entry(data=user_input)
        return self._async_option_form()

    @callback
    def _async_option_form(self):
        """Return configuration form for options."""
        options = _validate_options(self._std_options)

        opt_schema = {
            vol.Required(
                CONF_LOGO_OPTION,
                default=options.get(CONF_LOGO_OPTION, str(LOGO_OPTION_DEFAULT.value)),
            ): SelectSelector(_dict_to_select(LOGO_OPTIONS)),
            vol.Required(
                CONF_USE_LOCAL_LOGO,
                default=options.get(CONF_USE_LOCAL_LOGO, True),
            ): bool,
        }

        if not self._app_list:
            opt_schema.update(
                {
                    vol.Required(
                        CONF_APP_LOAD_METHOD,
                        default=options.get(
                            CONF_APP_LOAD_METHOD, str(AppLoadMethod.All.value)
                        ),
                    ): SelectSelector(_dict_to_select(APP_LOAD_METHODS)),
                }
            )

        if self._use_st:
            data_schema = vol.Schema(
                {
                    vol.Required(
                        CONF_USE_ST_STATUS_INFO,
                        default=options.get(CONF_USE_ST_STATUS_INFO, True),
                    ): bool,
                    vol.Required(
                        CONF_USE_ST_CHANNEL_INFO,
                        default=options.get(CONF_USE_ST_CHANNEL_INFO, True),
                    ): bool,
                    vol.Required(
                        CONF_SHOW_CHANNEL_NR,
                        default=options.get(CONF_SHOW_CHANNEL_NR, False),
                    ): bool,
                }
            ).extend(opt_schema)
            data_schema = data_schema.extend(
                {
                    vol.Required(
                        CONF_POWER_ON_METHOD,
                        default=options.get(
                            CONF_POWER_ON_METHOD, str(PowerOnMethod.WOL.value)
                        ),
                    ): SelectSelector(_dict_to_select(POWER_ON_METHODS)),
                }
            )
        else:
            data_schema = vol.Schema(opt_schema)

        if not self._adv_chk:
            data_schema = data_schema.extend(
                {vol.Required(CONF_SHOW_ADV_OPT, default=False): bool}
            )

        return self.async_show_form(step_id="init", data_schema=data_schema)

    async def async_step_menu(self, _=None):
        """Handle advanced options menu."""
        return self.async_show_menu(
            step_id="menu",
            menu_options=[
                "source_list",
                "app_list",
                "channel_list",
                "sync_ent",
                "init",
                "adv_opt",
                "save_exit",
            ],
        )

    async def async_step_save_exit(self, _) -> ConfigFlowResult:
        """Handle save and exit flow."""
        return self._save_entry(data=self._std_options)

    async def async_step_source_list(self, user_input=None):
        """Handle sources list flow."""
        errors: dict[str, str] | None = None
        if user_input is not None:
            valid_list = _validate_tv_list(user_input[CONF_SOURCE_LIST])
            if valid_list is not None:
                self._source_list = valid_list
                return await self.async_step_menu()
            errors = {CONF_BASE: "invalid_tv_list"}

        data_schema = vol.Schema(
            {
                vol.Optional(
                    CONF_SOURCE_LIST, default=self._source_list
                ): ObjectSelector()
            }
        )
        return self.async_show_form(
            step_id="source_list", data_schema=data_schema, errors=errors
        )

    async def async_step_app_list(self, user_input=None) -> ConfigFlowResult:
        """Handle apps list flow."""
        errors: dict[str, str] | None = None
        if user_input is not None:
            valid_list = _validate_tv_list(user_input[CONF_APP_LIST])
            if valid_list is not None:
                self._app_list = valid_list
                return await self.async_step_menu()
            errors = {CONF_BASE: "invalid_tv_list"}

        data_schema = vol.Schema(
            {vol.Optional(CONF_APP_LIST, default=self._app_list): ObjectSelector()}
        )
        return self.async_show_form(
            step_id="app_list", data_schema=data_schema, errors=errors
        )

    async def async_step_channel_list(self, user_input=None) -> ConfigFlowResult:
        """Handle channels list flow."""
        errors: dict[str, str] | None = None
        if user_input is not None:
            valid_list = _validate_tv_list(user_input[CONF_CHANNEL_LIST])
            if valid_list is not None:
                self._channel_list = valid_list
                return await self.async_step_menu()
            errors = {CONF_BASE: "invalid_tv_list"}

        data_schema = vol.Schema(
            {
                vol.Optional(
                    CONF_CHANNEL_LIST, default=self._channel_list
                ): ObjectSelector()
            }
        )
        return self.async_show_form(
            step_id="channel_list", data_schema=data_schema, errors=errors
        )

    async def async_step_sync_ent(self, user_input=None) -> ConfigFlowResult:
        """Handle syncronized entity flow."""
        if user_input is not None:
            self._sync_ent_opt = user_input
            return await self.async_step_menu()
        return self._async_sync_ent_form()

    @callback
    def _async_sync_ent_form(self) -> ConfigFlowResult:
        """Return configuration form for syncronized entity."""
        select_entities = EntitySelectorConfig(
            domain=_async_get_domains_service(self.hass, SERVICE_TURN_ON),
            exclude_entities=_async_get_entry_entities(self.hass, self._entry_id),
            multiple=True,
        )
        options = _validate_options(self._sync_ent_opt)

        data_schema = vol.Schema(
            {
                vol.Optional(
                    CONF_SYNC_TURN_OFF,
                    description={
                        "suggested_value": options.get(CONF_SYNC_TURN_OFF, [])
                    },
                ): EntitySelector(select_entities),
                vol.Optional(
                    CONF_SYNC_TURN_ON,
                    description={"suggested_value": options.get(CONF_SYNC_TURN_ON, [])},
                ): EntitySelector(select_entities),
            }
        )
        return self.async_show_form(step_id="sync_ent", data_schema=data_schema)

    async def async_step_adv_opt(self, user_input=None) -> ConfigFlowResult:
        """Handle advanced options flow."""
        if user_input is not None:
            self._adv_options = user_input
            return await self.async_step_menu()
        return self._async_adv_opt_form()

    @callback
    def _async_adv_opt_form(self) -> ConfigFlowResult:
        """Return configuration form for advanced options."""
        select_entities = EntitySelectorConfig(domain=BS_DOMAIN)
        options = _validate_options(self._adv_options)

        data_schema = vol.Schema(
            {
                vol.Required(
                    CONF_APP_LAUNCH_METHOD,
                    default=options.get(
                        CONF_APP_LAUNCH_METHOD, str(AppLaunchMethod.Standard.value)
                    ),
                ): SelectSelector(_dict_to_select(APP_LAUNCH_METHODS)),
                vol.Required(
                    CONF_WOL_REPEAT,
                    default=min(options.get(CONF_WOL_REPEAT, 1), MAX_WOL_REPEAT),
                ): vol.All(vol.Coerce(int), vol.Clamp(min=1, max=MAX_WOL_REPEAT)),
                vol.Required(
                    CONF_PING_PORT, default=options.get(CONF_PING_PORT, 0)
                ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=65535)),
                vol.Optional(
                    CONF_EXT_POWER_ENTITY,
                    description={
                        "suggested_value": options.get(CONF_EXT_POWER_ENTITY, "")
                    },
                ): EntitySelector(select_entities),
                vol.Required(
                    CONF_USE_MUTE_CHECK,
                    default=options.get(CONF_USE_MUTE_CHECK, False),
                ): bool,
                vol.Required(
                    CONF_DUMP_APPS,
                    default=options.get(CONF_DUMP_APPS, False),
                ): bool,
                vol.Required(
                    CONF_TOGGLE_ART_MODE,
                    default=options.get(CONF_TOGGLE_ART_MODE, False),
                ): bool,
            }
        )
        return self.async_show_form(step_id="adv_opt", data_schema=data_schema)


def _validate_options(options: dict) -> dict:
    """Validate options format"""
    valid_options = {}
    for opt_key, opt_val in options.items():
        if opt_key in [CONF_SYNC_TURN_OFF, CONF_SYNC_TURN_ON]:
            if not isinstance(opt_val, list):
                continue
        if opt_key in ENUM_OPTIONS:
            valid_options[opt_key] = str(opt_val)
        else:
            valid_options[opt_key] = opt_val
    return valid_options


def _validate_tv_list(input_list: dict[str, Any]) -> dict[str, str] | None:
    """Validate TV list from object selector."""
    valid_list = {}
    for name_val, id_val in input_list.items():
        if not id_val:
            continue
        if isinstance(id_val, Number):
            id_val = str(id_val)
        if not isinstance(id_val, str):
            return None
        valid_list[name_val] = id_val
    return valid_list


def _dict_to_select(opt_dict: dict) -> SelectSelectorConfig:
    """Covert a dict to a SelectSelectorConfig."""
    return SelectSelectorConfig(
        options=[SelectOptionDict(value=str(k), label=v) for k, v in opt_dict.items()],
        mode=SelectSelectorMode.DROPDOWN,
    )


def _async_get_domains_service(hass: HomeAssistant, service_name: str) -> list[str]:
    """Fetch list of domain that provide a specific service."""
    return [
        domain
        for domain, service in hass.services.async_services().items()
        if service_name in service
    ]


def _async_get_entry_entities(hass: HomeAssistant, entry_id: str) -> list[str]:
    """Get the entities related to current entry"""
    return [
        entry.entity_id
        for entry in (er.async_entries_for_config_entry(er.async_get(hass), entry_id))
    ]


================================================
FILE: custom_components/samsungtv_smart/const.py
================================================
"""Constants for the samsungtv_smart integration."""

from enum import Enum


class AppLoadMethod(Enum):
    """Valid application load methods."""

    All = 1
    Default = 2
    NotLoad = 3


class AppLaunchMethod(Enum):
    """Valid application launch methods."""

    Standard = 1
    Remote = 2
    Rest = 3


class PowerOnMethod(Enum):
    """Valid power on methods."""

    WOL = 1
    SmartThings = 2


DOMAIN = "samsungtv_smart"

MIN_HA_MAJ_VER = 2025
MIN_HA_MIN_VER = 6
__min_ha_version__ = f"{MIN_HA_MAJ_VER}.{MIN_HA_MIN_VER}.0"

DATA_CFG = "cfg"
DATA_CFG_YAML = "cfg_yaml"
DATA_OPTIONS = "options"
LOCAL_LOGO_PATH = "local_logo_path"
WS_PREFIX = "[Home Assistant]"

ATTR_DEVICE_MAC = "device_mac"
ATTR_DEVICE_MODEL = "device_model"
ATTR_DEVICE_NAME = "device_name"
ATTR_DEVICE_OS = "device_os"

CONF_APP_LAUNCH_METHOD = "app_launch_method"
CONF_APP_LIST = "app_list"
CONF_APP_LOAD_METHOD = "app_load_method"
CONF_CHANNEL_LIST = "channel_list"
CONF_DEVICE_MODEL = "device_model"
CONF_DEVICE_NAME = "device_name"
CONF_DEVICE_OS = "device_os"
CONF_DUMP_APPS = "dump_apps"
CONF_EXT_POWER_ENTITY = "ext_power_entity"
CONF_LOAD_ALL_APPS = "load_all_apps"
CONF_LOGO_OPTION = "logo_option"
CONF_PING_PORT = "ping_port"
CONF_POWER_ON_METHOD = "power_on_method"
CONF_SHOW_CHANNEL_NR = "show_channel_number"
CONF_SOURCE_LIST = "source_list"
CONF_SYNC_TURN_OFF = "sync_turn_off"
CONF_SYNC_TURN_ON = "sync_turn_on"
CONF_TOGGLE_ART_MODE = "toggle_art_mode"
CONF_USE_LOCAL_LOGO = "use_local_logo"
CONF_USE_MUTE_CHECK = "use_mute_check"
CONF_USE_ST_CHANNEL_INFO = "use_st_channel_info"
CONF_USE_ST_STATUS_INFO = "use_st_status_info"
CONF_WOL_REPEAT = "wol_repeat"
CONF_WS_NAME = "ws_name"

# for SmartThings integration api key usage
CONF_ST_ENTRY_UNIQUE_ID = "st_entry_unique_id"
CONF_USE_ST_INT_API_KEY = "use_st_int_api_key"  # obsolete used for migration

# obsolete
CONF_UPDATE_METHOD = "update_method"
CONF_UPDATE_CUSTOM_PING_URL = "update_custom_ping_url"
CONF_SCAN_APP_HTTP = "scan_app_http"

DEFAULT_APP = "TV/HDMI"
DEFAULT_PORT = 8001
DEFAULT_SOURCE_LIST = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"}
DEFAULT_TIMEOUT = 6

MAX_WOL_REPEAT = 5

RESULT_NOT_SUCCESSFUL = "not_successful"
RESULT_NOT_SUPPORTED = "not_supported"
RESULT_ST_DEVICE_USED = "st_device_used"
RESULT_ST_DEVICE_NOT_FOUND = "st_device_not_found"
RESULT_ST_MULTI_DEVICES = "st_multiple_device"
RESULT_SUCCESS = "success"
RESULT_WRONG_APIKEY = "wrong_api_key"

SERVICE_SELECT_PICTURE_MODE = "select_picture_mode"
SERVICE_SET_ART_MODE = "set_art_mode"

SIGNAL_CONFIG_ENTITY = f"{DOMAIN}_config"

STD_APP_LIST = {
    "org.tizen.browser": {
        "st_app_id": "",
        "logo": "tizenbrowser.png",
    },  # Internet
    "11101200001": {
        "st_app_id": "RN1MCdNq8t.Netflix",
        "logo": "netflix.png",
    },  # Netflix
    "3201907018807": {
        "st_app_id": "org.tizen.netflix-app",
        "logo": "netflix.png",
    },  # Netflix (New)
    "111299001912": {
        "st_app_id": "9Ur5IzDKqV.TizenYouTube",
        "logo": "youtube.png",
    },  # YouTube
    "3201512006785": {
        "st_app_id": "org.tizen.ignition",
        "logo": "primevideo.png",
    },  # Prime Video
    # "3201512006785": {
    #     "st_app_id": "evKhCgZelL.AmazonIgnitionLauncher2",
    #     "logo": "",
    # },  # Prime Video
    "3201901017640": {
        "st_app_id": "MCmYXNxgcu.DisneyPlus",
        "logo": "disneyplus.png",
    },  # Disney+
    "3202110025305": {
        "st_app_id": "rJyOSqC6Up.PPlusIntl",
        "logo": "paramountplus.png",
    },  # Paramount+
    "11091000000": {
        "st_app_id": "4ovn894vo9.Facebook",
        "logo": "facebook.png",
    },  # Facebook
    "3201806016390": {
        "st_app_id": "yu1NM3vHsU.DAZN",
        "logo": "dazn.png",
    },  # Dazn
    "3201601007250": {
        "st_app_id": "QizQxC7CUf.PlayMovies",
        "logo": "",
    },  # Google Play
    "3201606009684": {
        "st_app_id": "rJeHak5zRg.Spotify",
        "logo": "spotify.png",
    },  # Spotify
    "3201512006963": {
        "st_app_id": "kIciSQlYEM.plex",
        "logo": "",
    },  # Plex
}


================================================
FILE: custom_components/samsungtv_smart/diagnostics.py
================================================
"""Diagnostics support for Samsung TV Smart."""

from __future__ import annotations

from homeassistant.components.diagnostics import REDACTED, async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_MAC, CONF_TOKEN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er

from .const import DOMAIN

TO_REDACT = {CONF_API_KEY, CONF_MAC, CONF_TOKEN}


async def async_get_config_entry_diagnostics(
    hass: HomeAssistant, entry: ConfigEntry
) -> dict:
    """Return diagnostics for a config entry."""
    diag_data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)}

    yaml_data = hass.data[DOMAIN].get(entry.unique_id, {})
    if yaml_data:
        diag_data["config_data"] = async_redact_data(yaml_data, TO_REDACT)

    device_id = entry.data.get(CONF_ID, entry.entry_id)
    hass_data = _async_device_ha_info(hass, device_id)
    if hass_data:
        diag_data["device"] = hass_data

    return diag_data


@callback
def _async_device_ha_info(hass: HomeAssistant, device_id: str) -> dict | None:
    """Gather information how this TV device is represented in Home Assistant."""

    device_registry = dr.async_get(hass)
    entity_registry = er.async_get(hass)
    hass_device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)})
    if not hass_device:
        return None

    data = {
        "name": hass_device.name,
        "name_by_user": hass_device.name_by_user,
        "model": hass_device.model,
        "manufacturer": hass_device.manufacturer,
        "sw_version": hass_device.sw_version,
        "disabled": hass_device.disabled,
        "disabled_by": hass_device.disabled_by,
        "entities": {},
    }

    hass_entities = er.async_entries_for_device(
        entity_registry,
        device_id=hass_device.id,
        include_disabled_entities=True,
    )

    for entity_entry in hass_entities:
        if entity_entry.platform != DOMAIN:
            continue
        state = hass.states.get(entity_entry.entity_id)
        state_dict = None
        if state:
            state_dict = dict(state.as_dict())
            # The entity_id is already provided at root level.
            state_dict.pop("entity_id", None)
            # The context doesn't provide useful information in this case.
            state_dict.pop("context", None)
            # Redact the `entity_picture` attribute as it contains a token.
            if "entity_picture" in state_dict["attributes"]:
                state_dict["attributes"] = {
                    **state_dict["attributes"],
                    "entity_picture": REDACTED,
                }

        data["entities"][entity_entry.entity_id] = {
            "name": entity_entry.name,
            "original_name": entity_entry.original_name,
            "disabled": entity_entry.disabled,
            "disabled_by": entity_entry.disabled_by,
            "entity_category": entity_entry.entity_category,
            "device_class": entity_entry.device_class,
            "original_device_class": entity_entry.original_device_class,
            "icon": entity_entry.icon,
            "original_icon": entity_entry.original_icon,
            "unit_of_measurement": entity_entry.unit_of_measurement,
            "state": state_dict,
        }

    return data


================================================
FILE: custom_components/samsungtv_smart/entity.py
================================================
"""Base SamsungTV Entity."""

from __future__ import annotations

from typing import Any

from homeassistant.const import (
    ATTR_CONNECTIONS,
    ATTR_IDENTIFIERS,
    ATTR_SW_VERSION,
    CONF_HOST,
    CONF_ID,
    CONF_MAC,
    CONF_NAME,
)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import DeviceInfo, Entity

from .const import CONF_DEVICE_MODEL, CONF_DEVICE_NAME, CONF_DEVICE_OS, DOMAIN


class SamsungTVEntity(Entity):
    """Defines a base SamsungTV entity."""

    _attr_has_entity_name = True

    def __init__(self, config: dict[str, Any], entry_id: str) -> None:
        """Initialize the class."""
        self._name = config.get(CONF_NAME, config[CONF_HOST])
        self._mac = config.get(CONF_MAC)
        self._attr_unique_id = config.get(CONF_ID, entry_id)

        model = config.get(CONF_DEVICE_MODEL, "Samsung TV")
        if dev_name := config.get(CONF_DEVICE_NAME):
            model = f"{model} ({dev_name})"

        self._attr_device_info = DeviceInfo(
            manufacturer="Samsung Electronics",
            model=model,
            name=self._name,
        )
        if self.unique_id:
            self._attr_device_info[ATTR_IDENTIFIERS] = {(DOMAIN, self.unique_id)}
        if dev_os := config.get(CONF_DEVICE_OS):
            self._attr_device_info[ATTR_SW_VERSION] = dev_os
        if self._mac:
            self._attr_device_info[ATTR_CONNECTIONS] = {
                (CONNECTION_NETWORK_MAC, self._mac)
            }


================================================
FILE: custom_components/samsungtv_smart/logo.py
================================================
"""Logo implementation for SamsungTV Smart."""

import asyncio
from datetime import datetime, timedelta, timezone
from enum import Enum
import json
import logging
import os
from pathlib import Path
import re
import traceback
from typing import Optional

import aiofiles
from aiofiles import os as aiopath
import aiohttp

from .const import DOMAIN


# Logo feature constants
class LogoOption(Enum):
    """List of posible logo options."""

    Disabled = 1
    WhiteColor = 2
    BlueColor = 3
    BlueWhite = 4
    DarkWhite = 5
    TransparentColor = 6
    TransparentWhite = 7


CUSTOM_IMAGE_BASE_URL = f"/api/{DOMAIN}/custom"
STATIC_IMAGE_BASE_URL = f"/api/{DOMAIN}/static"
CHAR_REPLACE = {" ": "", "+": "plus", "_": "", ".": "", ":": ""}

LOGO_OPTIONS_MAPPING = {
    LogoOption.Disabled: "none",
    LogoOption.WhiteColor: "fff-color",
    LogoOption.BlueColor: "05a9f4-color",
    LogoOption.BlueWhite: "05a9f4-white",
    LogoOption.DarkWhite: "282c34-white",
    LogoOption.TransparentColor: "transparent-color",
    LogoOption.TransparentWhite: "transparent-white",
}
LOGO_OPTION_DEFAULT = LogoOption.WhiteColor
LOGO_BASE_URL = "https://jaruba.github.io/channel-logos/"
LOGO_FILE = "logo_paths.json"
LOGO_FILE_DOWNLOAD = "logo_paths_download.json"
LOGO_FILE_DAYS_BEFORE_UPDATE = 1
LOGO_MIN_SCORE_REQUIRED = 80
LOGO_MEDIATITLE_KEYWORD_REMOVAL = ["HDTV", "HD"]
LOGO_MAX_PATHS = 30000
LOGO_NO_MATCH = "NO_MATCH"
MAX_LOGO_CACHE = 200

_LOGGER = logging.getLogger(__name__)


class LocalImageUrl:
    """Class to manage the local image url."""

    def __init__(self, custom_logo_path=None):
        """Initialise the local image url class."""
        self._custom_logo_path = custom_logo_path
        self._local_image_url = None
        self._last_media_title = None

    def get_image_url(self, media_title, local_logo_file=None):
        """Check local image is present."""
        if not media_title and not local_logo_file:
            return None

        cf_local_logo_file = None
        if local_logo_file:
            cf_local_logo_file = local_logo_file.casefold()
            if cf_local_logo_file == self._last_media_title:
                return self._local_image_url
        if media_title == self._last_media_title:
            return self._local_image_url

        self._last_media_title = cf_local_logo_file or media_title
        self._local_image_url = None

        media_logo_file = media_title
        for searcher, replacer in CHAR_REPLACE.items():
            media_logo_file = media_logo_file.replace(searcher, replacer)
        media_logo_file += ".png"

        if self._custom_logo_path:
            for logo_file in Path(self._custom_logo_path).iterdir():
                if logo_file.name.casefold() == media_logo_file.casefold():
                    self._local_image_url = f"{CUSTOM_IMAGE_BASE_URL}/{logo_file.name}"
                    self._last_media_title = media_title
                    break

        if not self._local_image_url and local_logo_file:
            dir_path = Path(__file__).parent / "static"
            for logo_file in Path(dir_path).iterdir():
                if logo_file.name.casefold() == local_logo_file.casefold():
                    self._local_image_url = f"{STATIC_IMAGE_BASE_URL}/{logo_file.name}"
                    break

        return self._local_image_url


class Logo:
    """
    Class that fetches logos for Samsung TV Tizen.
    Works with https://github.com/jaruba/channel-logos.
    """

    def __init__(
        self,
        logo_option: LogoOption,
        logo_file_download: str = None,
        session: Optional[aiohttp.ClientSession] = None,
    ):
        self._media_image_base_url = None
        self._logo_option = None
        self.set_logo_color(logo_option)
        if session:
            self._session = session
        else:
            self._session = aiohttp.ClientSession()

        self._images_paths = None
        self._logo_cache = {}
        self._last_check = None

        app_path = os.path.dirname(os.path.realpath(__file__))
        self._logo_file_path = os.path.join(app_path, LOGO_FILE)
        self._logo_file_download_path = logo_file_download or os.path.join(
            app_path, LOGO_FILE_DOWNLOAD
        )

    def set_logo_color(self, logo_type: LogoOption):
        """Sets the logo color option and image base url if not already set to this option"""
        logo_option = LOGO_OPTIONS_MAPPING[logo_type]
        if self._logo_option and self._logo_option == logo_option:
            return

        _LOGGER.debug("Setting logo option to %s", logo_option)
        self._logo_option = logo_option

        if logo_type == LogoOption.Disabled:
            self._media_image_base_url = None
        else:
            self._media_image_base_url = f"{LOGO_BASE_URL}export/{self._logo_option}"

    def check_requested(self):
        """Check if a new file update is requested."""
        if self._media_image_base_url is None:
            return False

        check_time = datetime.now(timezone.utc).astimezone()
        if self._last_check is not None and self._last_check > check_time - timedelta(
            days=LOGO_FILE_DAYS_BEFORE_UPDATE
        ):
            return False

        return True

    async def _async_ensure_latest_path_file(self):
        """Does check if logo paths file exists and if it does - is it out of date or not."""
        if not self.check_requested():
            return

        check_time = datetime.now(timezone.utc).astimezone()
        update_file = not await aiopath.path.isfile(self._logo_file_download_path)
        if not update_file:
            file_date = datetime.fromtimestamp(
                await aiopath.path.getmtime(self._logo_file_download_path), timezone.utc
            ).astimezone()
            if file_date > check_time - timedelta(days=LOGO_FILE_DAYS_BEFORE_UPDATE):
                self._last_check = file_date
                return

            try:
                async with self._session.head(
                    LOGO_BASE_URL + "logo_paths.json"
                ) as response:
                    url_date = datetime.strptime(
                        response.headers.get("Last-Modified"),
                        "%a, %d %b %Y %X %Z",
                    ).astimezone()
                    update_file = url_date > file_date
            except (aiohttp.ClientError, asyncio.TimeoutError):
                _LOGGER.warning(
                    "Not able to check for latest paths file for logos from %s%s. "
                    "Check if the URL is accessible from this machine",
                    LOGO_BASE_URL,
                    "logo_paths.json",
                )

        self._last_check = check_time
        if 
Download .txt
gitextract_jb3sp0ji/

├── .devcontainer/
│   └── devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── hassfest.yaml
│       ├── linting.yaml
│       ├── release.yml
│       ├── stale.yaml
│       └── validate.yaml
├── .gitignore
├── .prettierignore
├── .pylintrc
├── .ruff.toml
├── .vscode/
│   ├── extensions.json
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── Dockerfile.dev
├── LICENSE
├── README.md
├── config/
│   └── configuration.yaml
├── custom_components/
│   ├── __init__.py
│   └── samsungtv_smart/
│       ├── __init__.py
│       ├── api/
│       │   ├── __init__.py
│       │   ├── samsungcast.py
│       │   ├── samsungws.py
│       │   ├── shortcuts.py
│       │   ├── smartthings.py
│       │   └── upnp.py
│       ├── config_flow.py
│       ├── const.py
│       ├── diagnostics.py
│       ├── entity.py
│       ├── logo.py
│       ├── logo_paths.json
│       ├── manifest.json
│       ├── media_player.py
│       ├── remote.py
│       ├── services.yaml
│       └── translations/
│           ├── en.json
│           ├── hu.json
│           ├── it.json
│           └── pt-BR.json
├── docs/
│   ├── App_list.md
│   ├── Key_chaining.md
│   ├── Key_codes.md
│   └── Smartthings.md
├── hacs.json
├── info.md
├── requirements.txt
├── requirements_test.txt
├── script/
│   └── integration_init
├── scripts/
│   ├── develop
│   ├── lint
│   └── setup
├── setup.cfg
└── tests/
    ├── __init__.py
    └── conftest.py
Download .txt
SYMBOL INDEX (358 symbols across 14 files)

FILE: custom_components/samsungtv_smart/__init__.py
  function ensure_unique_hosts (line 110) | def ensure_unique_hosts(value):
  function tv_url (line 154) | def tv_url(host: str, address: str = "") -> str:
  function is_min_ha_version (line 159) | def is_min_ha_version(min_ha_major_ver: int, min_ha_minor_ver: int) -> b...
  function is_valid_ha_version (line 166) | def is_valid_ha_version() -> bool:
  function _notify_message (line 171) | def _notify_message(
  function _load_option_list (line 188) | def _load_option_list(src_list):
  function token_file_name (line 204) | def token_file_name(hostname: str) -> str:
  function _remove_token_file (line 209) | def _remove_token_file(hass, hostname, token_file=None):
  function _migrate_token (line 223) | def _migrate_token(hass: HomeAssistant, entry: ConfigEntry, hostname: st...
  function _migrate_options_format (line 251) | def _migrate_options_format(hass: HomeAssistant, entry: ConfigEntry) -> ...
  function _migrate_entry_unique_id (line 285) | def _migrate_entry_unique_id(hass: HomeAssistant, entry: ConfigEntry) ->...
  function _migrate_smartthings_config (line 315) | def _migrate_smartthings_config(hass: HomeAssistant, entry: ConfigEntry)...
  function get_smartthings_entries (line 330) | def get_smartthings_entries(hass: HomeAssistant) -> dict[str, str] | None:
  function get_smartthings_api_key (line 344) | def get_smartthings_api_key(hass: HomeAssistant, st_unique_id: str) -> s...
  function _register_logo_paths (line 360) | async def _register_logo_paths(hass: HomeAssistant) -> str | None:
  function get_device_info (line 390) | async def get_device_info(hostname: str, session: ClientSession) -> dict:
  class SamsungTVInfo (line 419) | class SamsungTVInfo:
    method __init__ (line 422) | def __init__(self, hass, hostname, ws_name):
    method ws_port (line 432) | def ws_port(self):
    method ws_token (line 437) | def ws_token(self):
    method ping_port (line 442) | def ping_port(self):
    method _try_connect_ws (line 446) | def _try_connect_ws(self):
    method _try_connect_st (line 497) | async def _try_connect_st(api_key, device_id, session: ClientSession):
    method get_st_devices (line 524) | async def get_st_devices(api_key, session: ClientSession, st_device_la...
    method try_connect (line 538) | async def try_connect(
  function async_setup (line 563) | async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
  function async_setup_entry (line 612) | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> ...
  function async_unload_entry (line 658) | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) ->...
  function async_remove_entry (line 671) | async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) ->...
  function _update_listener (line 680) | async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> N...

FILE: custom_components/samsungtv_smart/api/samsungcast.py
  function _format_url (line 14) | def _format_url(host: str, app: str) -> str:
  class CastTubeNotSupported (line 19) | class CastTubeNotSupported(Exception):
  class SamsungCastTube (line 23) | class SamsungCastTube:
    method __init__ (line 26) | def __init__(self, host: str):
    method _get_screen_id (line 32) | def _get_screen_id(host: str) -> str:
    method _get_api (line 56) | def _get_api(self) -> YouTubeSession:
    method play_video (line 63) | def play_video(self, video_id: str) -> None:
    method play_next (line 67) | def play_next(self, video_id: str) -> None:
    method add_to_queue (line 71) | def add_to_queue(self, video_id: str) -> None:
    method clear_queue (line 75) | def clear_queue(self) -> None:

FILE: custom_components/samsungtv_smart/api/samsungws.py
  function _set_ws_logger_level (line 64) | def _set_ws_logger_level(level: int = logging.CRITICAL) -> None:
  function _format_rest_url (line 71) | def _format_rest_url(host: str, append: str = "") -> str:
  function gen_uuid (line 76) | def gen_uuid() -> str:
  function aware_utcnow (line 81) | def aware_utcnow() -> datetime:
  function kill_subprocess (line 86) | def kill_subprocess(
  function _process_api_response (line 97) | def _process_api_response(response, *, raise_error=True):
  function _log_ping_pong (line 110) | def _log_ping_pong(msg, *args):
  class Ping (line 117) | class Ping:
    method __init__ (line 120) | def __init__(self, host):
    method ping (line 128) | def ping(self, port=0):
    method _ping (line 134) | def _ping(self):
    method _ping_socket (line 148) | def _ping_socket(self, port):
  class ConnectionFailure (line 155) | class ConnectionFailure(Exception):
  class ResponseError (line 159) | class ResponseError(Exception):
  class HttpApiError (line 163) | class HttpApiError(Exception):
  class App (line 167) | class App:
    method __init__ (line 170) | def __init__(self, app_id, app_name, app_type):
  class ArtModeStatus (line 176) | class ArtModeStatus(Enum):
  class SamsungTVAsyncRest (line 185) | class SamsungTVAsyncRest:
    method __init__ (line 188) | def __init__(
    method _rest_request (line 199) | async def _rest_request(self, target: str, method: str = "GET") -> dic...
    method async_rest_device_info (line 218) | async def async_rest_device_info(self) -> dict[str, Any]:
    method async_rest_app_status (line 223) | async def async_rest_app_status(self, app_id: str) -> dict[str, Any]:
    method async_rest_app_run (line 228) | async def async_rest_app_run(self, app_id: str) -> dict[str, Any]:
    method async_rest_app_close (line 233) | async def async_rest_app_close(self, app_id: str) -> dict[str, Any]:
    method async_rest_app_install (line 238) | async def async_rest_app_install(self, app_id: str) -> dict[str, Any]:
  class SamsungTVWS (line 244) | class SamsungTVWS:
    method __init__ (line 247) | def __init__(
    method __enter__ (line 309) | def __enter__(self):
    method __exit__ (line 312) | def __exit__(self, exc_type, exc_value, exc_traceback):
    method ping_probe (line 316) | def ping_probe(host):
    method _serialize_string (line 329) | def _serialize_string(string):
    method _is_ssl_connection (line 334) | def _is_ssl_connection(self):
    method _format_websocket_url (line 337) | def _format_websocket_url(self, path, is_ssl=False, use_token=True):
    method set_ping_port (line 349) | def set_ping_port(self, port: int):
    method update_app_list (line 353) | def update_app_list(self, app_list: dict | None):
    method register_new_token_callback (line 357) | def register_new_token_callback(self, func):
    method register_status_callback (line 361) | def register_status_callback(self, func):
    method unregister_status_callback (line 365) | def unregister_status_callback(self):
    method _get_token (line 369) | def _get_token(self):
    method _set_token (line 380) | def _set_token(self, token):
    method _ws_send (line 395) | def _ws_send(
    method _rest_request (line 445) | def _rest_request(self, target, method="GET"):
    method _check_conn_id (line 463) | def _check_conn_id(self, resp_data):
    method _run_forever (line 483) | def _run_forever(
    method _client_remote_thread (line 490) | def _client_remote_thread(self):
    method _on_ping_remote (line 520) | def _on_ping_remote(self, _, payload):
    method _on_message_remote (line 530) | def _on_message_remote(self, _, message):
    method _request_apps_list (line 561) | def _request_apps_list(self):
    method _handle_installed_app (line 572) | def _handle_installed_app(self, response):
    method _client_control_thread (line 583) | def _client_control_thread(self):
    method _on_ping_control (line 610) | def _on_ping_control(self, _, payload):
    method _on_message_control (line 620) | def _on_message_control(self, _, message):
    method _set_running_app (line 646) | def _set_running_app(self, response):
    method _manage_control_err (line 677) | def _manage_control_err(self, response):
    method _get_app_status (line 696) | def _get_app_status(self, app_id, app_type):
    method _client_art_thread (line 722) | def _client_art_thread(self):
    method _on_ping_art (line 747) | def _on_ping_art(self, _, payload):
    method _on_message_art (line 757) | def _on_message_art(self, _, message):
    method _get_artmode_status (line 781) | def _get_artmode_status(self):
    method _handle_artmode_status (line 802) | def _handle_artmode_status(self, response):
    method is_connected (line 840) | def is_connected(self):
    method artmode_status (line 845) | def artmode_status(self):
    method installed_app (line 850) | def installed_app(self):
    method running_app (line 855) | def running_app(self):
    method is_app_running (line 859) | def is_app_running(self, app_id: str) -> bool | None:
    method _ping_thread_method (line 871) | def _ping_thread_method(self):
    method _check_remote (line 885) | def _check_remote(self):
    method _check_art_mode (line 904) | def _check_art_mode(self):
    method _notify_app_change (line 916) | def _notify_app_change(self):
    method _get_running_app (line 928) | def _get_running_app(self, *, force_scan=False):
    method set_power_on_request (line 959) | def set_power_on_request(self, set_art_mode=False, power_on_delay=0):
    method set_power_off_request (line 966) | def set_power_off_request(self):
    method start_poll (line 970) | def start_poll(self):
    method stop_poll (line 979) | def stop_poll(self):
    method _start_client (line 988) | def _start_client(self, *, start_all=False):
    method stop_client (line 1016) | def stop_client(self):
    method open (line 1021) | def open(self):
    method close (line 1056) | def close(self):
    method send_key (line 1063) | def send_key(self, key, key_press_delay=None, cmd="Click"):
    method hold_key (line 1079) | def hold_key(self, key, seconds):
    method send_text (line 1086) | def send_text(self, text, send_delay=None):
    method move_cursor (line 1116) | def move_cursor(self, x, y, duration=0):
    method run_app (line 1130) | def run_app(self, app_id, action_type="", meta_tag="", *, use_remote=F...
    method open_browser (line 1179) | def open_browser(self, url):
    method rest_device_info (line 1184) | def rest_device_info(self):
    method rest_app_status (line 1189) | def rest_app_status(self, app_id):
    method rest_app_run (line 1194) | def rest_app_run(self, app_id):
    method rest_app_close (line 1199) | def rest_app_close(self, app_id):
    method rest_app_install (line 1204) | def rest_app_install(self, app_id):
    method shortcuts (line 1209) | def shortcuts(self):

FILE: custom_components/samsungtv_smart/api/shortcuts.py
  class SamsungTVShortcuts (line 24) | class SamsungTVShortcuts:
    method __init__ (line 25) | def __init__(self, remote):
    method power (line 29) | def power(self):
    method home (line 33) | def home(self):
    method menu (line 36) | def menu(self):
    method source (line 39) | def source(self):
    method guide (line 42) | def guide(self):
    method tools (line 45) | def tools(self):
    method info (line 48) | def info(self):
    method up (line 52) | def up(self):
    method down (line 55) | def down(self):
    method left (line 58) | def left(self):
    method right (line 61) | def right(self):
    method enter (line 64) | def enter(self, count=1):
    method back (line 67) | def back(self):
    method channel_list (line 71) | def channel_list(self):
    method channel (line 74) | def channel(self, ch):
    method digit (line 80) | def digit(self, d):
    method channel_up (line 83) | def channel_up(self):
    method channel_down (line 86) | def channel_down(self):
    method volume_up (line 90) | def volume_up(self):
    method volume_down (line 93) | def volume_down(self):
    method mute (line 96) | def mute(self):
    method red (line 100) | def red(self):
    method green (line 103) | def green(self):
    method yellow (line 106) | def yellow(self):
    method blue (line 109) | def blue(self):

FILE: custom_components/samsungtv_smart/api/smartthings.py
  function _headers (line 111) | def _headers(api_key: str) -> dict[str, str]:
  function _command (line 119) | def _command(command: dict, arguments: list | None = None):
  class STStatus (line 127) | class STStatus(Enum):
  class SmartThingsTV (line 135) | class SmartThingsTV:
    method __init__ (line 138) | def __init__(
    method __enter__ (line 177) | def __enter__(self):
    method __exit__ (line 180) | def __exit__(self, ext_type, ext_value, ext_traceback):
    method _get_api_key (line 183) | def _get_api_key(self) -> str:
    method api_key (line 191) | def api_key(self) -> str:
    method device_id (line 196) | def device_id(self) -> str:
    method device_name (line 201) | def device_name(self) -> str:
    method state (line 206) | def state(self):
    method prev_state (line 211) | def prev_state(self):
    method muted (line 216) | def muted(self) -> bool:
    method volume (line 221) | def volume(self) -> int:
    method source (line 226) | def source(self) -> str:
    method channel (line 231) | def channel(self) -> str:
    method channel_name (line 236) | def channel_name(self) -> str:
    method source_list (line 241) | def source_list(self):
    method sound_mode (line 246) | def sound_mode(self):
    method sound_mode_list (line 253) | def sound_mode_list(self):
    method picture_mode (line 260) | def picture_mode(self):
    method picture_mode_list (line 267) | def picture_mode_list(self):
    method get_source_name (line 273) | def get_source_name(self, source_id: str) -> str:
    method _get_source_list_from_map (line 285) | def _get_source_list_from_map(self) -> list:
    method set_application (line 298) | def set_application(self, app_id):
    method _set_source (line 306) | def _set_source(self, source):
    method _load_json_list (line 316) | def _load_json_list(dev_data, list_name):
    method get_devices_list (line 328) | async def get_devices_list(api_key, session: ClientSession, device_lab...
    method _device_refresh (line 366) | async def _device_refresh(self, **kwargs):
    method _async_send_command (line 391) | async def _async_send_command(self, data_cmd):
    method async_device_health (line 412) | async def async_device_health(self):
    method async_device_update (line 436) | async def async_device_update(self, use_channel_info: bool = None):
    method async_turn_off (line 535) | async def async_turn_off(self):
    method async_turn_on (line 540) | async def async_turn_on(self):
    method async_send_command (line 545) | async def async_send_command(self, cmd_type, command=""):
    method async_select_source (line 573) | async def async_select_source(self, source):
    method async_select_vd_source (line 582) | async def async_select_vd_source(self, source):
    method async_set_sound_mode (line 589) | async def async_set_sound_mode(self, mode):
    method async_set_picture_mode (line 599) | async def async_set_picture_mode(self, mode):
  class InvalidSmartThingsSoundMode (line 610) | class InvalidSmartThingsSoundMode(RuntimeError):
  class InvalidSmartThingsPictureMode (line 614) | class InvalidSmartThingsPictureMode(RuntimeError):

FILE: custom_components/samsungtv_smart/api/upnp.py
  class SamsungUPnP (line 15) | class SamsungUPnP:
    method __init__ (line 18) | def __init__(self, host, session: Optional[ClientSession] = None):
    method _soap_request (line 29) | async def _soap_request(
    method connected (line 64) | def connected(self):
    method async_disconnect (line 68) | async def async_disconnect(self):
    method async_get_volume (line 73) | async def async_get_volume(self):
    method async_set_volume (line 87) | async def async_set_volume(self, volume):
    method async_get_mute (line 95) | async def async_get_mute(self):
    method async_set_current_media (line 111) | async def async_set_current_media(self, url):
    method async_play (line 128) | async def async_play(self):

FILE: custom_components/samsungtv_smart/config_flow.py
  function _get_ip (line 148) | def _get_ip(host):
  class SamsungTVConfigFlow (line 157) | class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
    method __init__ (line 164) | def __init__(self) -> None:
    method _stdev_already_used (line 182) | def _stdev_already_used(self, devices_id) -> bool:
    method _remove_stdev_used (line 189) | def _remove_stdev_used(self, devices_list: Dict[str, Any]) -> Dict[str...
    method _extract_dev_name (line 199) | def _extract_dev_name(device) -> str:
    method _prepare_dev_schema (line 207) | def _prepare_dev_schema(self, devices_list) -> vol.Schema:
    method _get_st_deviceid (line 215) | async def _get_st_deviceid(self, st_device_label="") -> str:
    method _try_connect (line 233) | async def _try_connect(self, *, port=None, token=None, skip_info=False...
    method _get_api_key (line 250) | def _get_api_key(self) -> str | None:
    method async_step_user (line 258) | async def async_step_user(
    method async_step_stdevice (line 325) | async def async_step_stdevice(
    method async_step_stdeviceid (line 337) | async def async_step_stdeviceid(
    method async_step_reconfigure (line 355) | async def async_step_reconfigure(
    method _manage_result (line 404) | async def _manage_result(self, result: str, is_user_step=False) -> Con...
    method _manage_reconfigure (line 427) | def _manage_reconfigure(self, result: str) -> ConfigFlowResult:
    method _save_entry (line 452) | def _save_entry(self) -> ConfigFlowResult:
    method _get_init_schema (line 488) | def _get_init_schema(self) -> vol.Schema:
    method _get_reconfigure_schema (line 519) | def _get_reconfigure_schema(self) -> vol.Schema:
    method _show_form (line 555) | def _show_form(
    method async_get_options_flow (line 579) | def async_get_options_flow(config_entry) -> OptionsFlowHandler:
  class OptionsFlowHandler (line 584) | class OptionsFlowHandler(OptionsFlow):
    method __init__ (line 587) | def __init__(self, config_entry: ConfigEntry) -> None:
    method _save_entry (line 610) | def _save_entry(self, data) -> ConfigFlowResult:
    method async_step_init (line 624) | async def async_step_init(self, user_input=None) -> ConfigFlowResult:
    method _async_option_form (line 635) | def _async_option_form(self):
    method async_step_menu (line 699) | async def async_step_menu(self, _=None):
    method async_step_save_exit (line 714) | async def async_step_save_exit(self, _) -> ConfigFlowResult:
    method async_step_source_list (line 718) | async def async_step_source_list(self, user_input=None):
    method async_step_app_list (line 739) | async def async_step_app_list(self, user_input=None) -> ConfigFlowResult:
    method async_step_channel_list (line 756) | async def async_step_channel_list(self, user_input=None) -> ConfigFlow...
    method async_step_sync_ent (line 777) | async def async_step_sync_ent(self, user_input=None) -> ConfigFlowResult:
    method _async_sync_ent_form (line 785) | def _async_sync_ent_form(self) -> ConfigFlowResult:
    method async_step_adv_opt (line 810) | async def async_step_adv_opt(self, user_input=None) -> ConfigFlowResult:
    method _async_adv_opt_form (line 818) | def _async_adv_opt_form(self) -> ConfigFlowResult:
  function _validate_options (line 861) | def _validate_options(options: dict) -> dict:
  function _validate_tv_list (line 875) | def _validate_tv_list(input_list: dict[str, Any]) -> dict[str, str] | None:
  function _dict_to_select (line 889) | def _dict_to_select(opt_dict: dict) -> SelectSelectorConfig:
  function _async_get_domains_service (line 897) | def _async_get_domains_service(hass: HomeAssistant, service_name: str) -...
  function _async_get_entry_entities (line 906) | def _async_get_entry_entities(hass: HomeAssistant, entry_id: str) -> lis...

FILE: custom_components/samsungtv_smart/const.py
  class AppLoadMethod (line 6) | class AppLoadMethod(Enum):
  class AppLaunchMethod (line 14) | class AppLaunchMethod(Enum):
  class PowerOnMethod (line 22) | class PowerOnMethod(Enum):

FILE: custom_components/samsungtv_smart/diagnostics.py
  function async_get_config_entry_diagnostics (line 16) | async def async_get_config_entry_diagnostics(
  function _async_device_ha_info (line 35) | def _async_device_ha_info(hass: HomeAssistant, device_id: str) -> dict |...

FILE: custom_components/samsungtv_smart/entity.py
  class SamsungTVEntity (line 22) | class SamsungTVEntity(Entity):
    method __init__ (line 27) | def __init__(self, config: dict[str, Any], entry_id: str) -> None:

FILE: custom_components/samsungtv_smart/logo.py
  class LogoOption (line 22) | class LogoOption(Enum):
  class LocalImageUrl (line 61) | class LocalImageUrl:
    method __init__ (line 64) | def __init__(self, custom_logo_path=None):
    method get_image_url (line 70) | def get_image_url(self, media_title, local_logo_file=None):
  class Logo (line 108) | class Logo:
    method __init__ (line 114) | def __init__(
    method set_logo_color (line 138) | def set_logo_color(self, logo_type: LogoOption):
    method check_requested (line 152) | def check_requested(self):
    method _async_ensure_latest_path_file (line 165) | async def _async_ensure_latest_path_file(self):
    method _download_latest_path_file (line 202) | async def _download_latest_path_file(self):
    method _read_path_file (line 238) | async def _read_path_file(self, force_read=False):
    method _add_to_cache (line 268) | def _add_to_cache(self, media_title, logo_path=LOGO_NO_MATCH):
    method async_find_match (line 274) | async def async_find_match(self, media_title):
  function _levenshtein_ratio (line 360) | def _levenshtein_ratio(s: str, t: str):

FILE: custom_components/samsungtv_smart/media_player.py
  function async_setup_entry (line 161) | async def async_setup_entry(
  function _get_default_app_info (line 208) | def _get_default_app_info(app_id):
  class ArtModeSupport (line 224) | class ArtModeSupport(Enum):
  class SamsungTVDevice (line 232) | class SamsungTVDevice(SamsungTVEntity, MediaPlayerEntity):
    method __init__ (line 238) | def __init__(
    method _update_smartthing_token (line 371) | def _update_smartthing_token(
    method async_added_to_hass (line 390) | async def async_added_to_hass(self):
    method async_will_remove_from_hass (line 408) | async def async_will_remove_from_hass(self):
    method _split_app_list (line 414) | def _split_app_list(app_list: dict[str, str]) -> list[dict[str, str]]:
    method _load_tv_lists (line 442) | def _load_tv_lists(self, first_load=False):
    method _update_config_options (line 468) | def _update_config_options(self, first_load=False):
    method _status_changed_callback (line 479) | def _status_changed_callback(self):
    method _get_option (line 484) | def _get_option(self, param, default=None):
    method _get_device_spec (line 491) | def _get_device_spec(self, key: str) -> Any | None:
    method _power_off_in_progress (line 497) | def _power_off_in_progress(self):
    method _update_volume_info (line 504) | async def _update_volume_info(self):
    method _get_external_entity_status (line 518) | def _get_external_entity_status(self):
    method _check_status (line 524) | async def _check_status(self):
    method _get_running_app (line 553) | def _get_running_app(self):
    method _get_st_sources (line 571) | def _get_st_sources(self):
    method _gen_installed_app_list (line 623) | def _gen_installed_app_list(self):
    method _get_source (line 677) | def _get_source(self):
    method _smartthings_keys (line 702) | async def _smartthings_keys(self, source_key: str):
    method _log_st_error (line 747) | def _log_st_error(self, st_error: bool):
    method _async_load_device_info (line 778) | async def _async_load_device_info(
    method _async_st_update (line 796) | async def _async_st_update(self, **kwargs) -> bool | None:
    method async_update (line 813) | async def async_update(self):
    method send_command (line 870) | def send_command(
    method async_send_command (line 949) | async def async_send_command(
    method _update_media (line 961) | async def _update_media(self):
    method _get_new_media_title (line 1003) | def _get_new_media_title(self):
    method _local_media_image (line 1033) | async def _local_media_image(self, media_title):
    method supported_features (line 1048) | def supported_features(self) -> int:
    method extra_state_attributes (line 1058) | def extra_state_attributes(self):
    method media_channel (line 1075) | def media_channel(self):
    method media_content_type (line 1084) | def media_content_type(self):
    method app_id (line 1095) | def app_id(self):
    method state (line 1110) | def state(self):
    method source_list (line 1123) | def source_list(self):
    method channel_list (line 1140) | def channel_list(self):
    method source (line 1147) | def source(self):
    method sound_mode (line 1152) | def sound_mode(self):
    method sound_mode_list (line 1159) | def sound_mode_list(self):
    method support_art_mode (line 1166) | def support_art_mode(self) -> ArtModeSupport:
    method _send_wol_packet (line 1174) | def _send_wol_packet(self, wol_repeat=None):
    method _async_power_on (line 1203) | async def _async_power_on(self, set_art_mode=False):
    method _async_turn_on (line 1242) | async def _async_turn_on(self, set_art_mode=False):
    method async_turn_on (line 1254) | async def async_turn_on(self):
    method async_set_art_mode (line 1258) | async def async_set_art_mode(self):
    method _turn_off (line 1268) | def _turn_off(self):
    method async_turn_off (line 1290) | async def async_turn_off(self):
    method async_toggle (line 1296) | async def async_toggle(self):
    method async_volume_up (line 1307) | async def async_volume_up(self):
    method async_volume_down (line 1315) | async def async_volume_down(self):
    method async_mute_volume (line 1323) | async def async_mute_volume(self, mute):
    method async_set_volume_level (line 1333) | async def async_set_volume_level(self, volume):
    method media_play_pause (line 1345) | def media_play_pause(self):
    method media_play (line 1352) | def media_play(self):
    method media_pause (line 1357) | def media_pause(self):
    method media_stop (line 1362) | def media_stop(self):
    method media_next_track (line 1367) | def media_next_track(self):
    method media_previous_track (line 1374) | def media_previous_track(self):
    method _async_send_keys (line 1381) | async def _async_send_keys(self, source_key):
    method _async_set_channel_source (line 1413) | async def _async_set_channel_source(self, channel_source=None):
    method _async_set_channel (line 1437) | async def _async_set_channel(self, channel):
    method _async_launch_app (line 1471) | async def _async_launch_app(self, app_data, meta_data=None):
    method _get_youtube_app_id (line 1502) | def _get_youtube_app_id(self):
    method _get_youtube_video_id (line 1520) | def _get_youtube_video_id(self, url):
    method _cast_youtube_video (line 1547) | def _cast_youtube_video(self, video_id: str, enqueue: MediaPlayerEnque...
    method _async_play_youtube_video (line 1562) | async def _async_play_youtube_video(
    method async_play_media (line 1579) | async def async_play_media(
    method async_browse_media (line 1645) | async def async_browse_media(self, media_content_type=None, media_cont...
    method async_select_source (line 1649) | async def async_select_source(self, source):
    method _async_select_source_delayed (line 1681) | async def _async_select_source_delayed(self, source):
    method async_select_sound_mode (line 1690) | async def async_select_sound_mode(self, sound_mode):
    method async_select_picture_mode (line 1696) | async def async_select_picture_mode(self, picture_mode):
    method _async_switch_entity (line 1702) | async def _async_switch_entity(self, power_on: bool):
  function _async_call_service (line 1729) | async def _async_call_service(

FILE: custom_components/samsungtv_smart/remote.py
  function async_setup_entry (line 40) | async def async_setup_entry(
  class SamsungTVRemote (line 66) | class SamsungTVRemote(SamsungTVEntity, RemoteEntity):
    method __init__ (line 72) | def __init__(self, config: dict[str, Any], entry_id: str, mp_entity_id...
    method _async_call_service (line 77) | async def _async_call_service(
    method async_turn_off (line 103) | async def async_turn_off(self, **kwargs: Any) -> None:
    method async_turn_on (line 108) | async def async_turn_on(self, **kwargs: Any) -> None:
    method async_send_command (line 113) | async def async_send_command(self, command: Iterable[str], **kwargs: A...

FILE: tests/conftest.py
  function auto_enable_custom_integrations (line 28) | def auto_enable_custom_integrations(enable_custom_integrations):
  function skip_notifications_fixture (line 36) | def skip_notifications_fixture():
Condensed preview — 59 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (417K chars).
[
  {
    "path": ".devcontainer/devcontainer.json",
    "chars": 1348,
    "preview": "{\n  \"name\": \"SamsungTV Smart Integration\",\n  \"dockerFile\": \"../Dockerfile.dev\",\n  \"postCreateCommand\": \"scripts/setup\",\n"
  },
  {
    "path": ".dockerignore",
    "chars": 185,
    "preview": "# General files\n.git\n.github\nconfig\ndocs\n\n# Development\n.devcontainer\n.vscode\n\n# Test related files\ntests\n\n# Other virtu"
  },
  {
    "path": ".gitattributes",
    "chars": 301,
    "preview": "# Ensure Docker script files uses LF to support Docker for Windows.\n# Ensure \"git config --global core.autocrlf input\" b"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 684,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 608,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: Feature Request\nassignees: ''\n\n---\n\n"
  },
  {
    "path": ".github/workflows/hassfest.yaml",
    "chars": 302,
    "preview": "name: Validate with Hassfest\n\non:\n  push:\n    branches:\n      - master\n\n  pull_request:\n    branches: [\"*\"]\n\n  schedule:"
  },
  {
    "path": ".github/workflows/linting.yaml",
    "chars": 556,
    "preview": "name: Linting\n\non:\n  push:\n    branches:\n      - master\n\n  pull_request:\n    branches: [\"*\"]\n\njobs:\n  lint:\n    runs-on:"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 682,
    "preview": "name: \"Release\"\n\non:\n  release:\n    types:\n      - \"published\"\n\npermissions: {}\n\njobs:\n  release:\n    name: \"Release\"\n  "
  },
  {
    "path": ".github/workflows/stale.yaml",
    "chars": 1396,
    "preview": "# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.\n#\n# You c"
  },
  {
    "path": ".github/workflows/validate.yaml",
    "chars": 352,
    "preview": "name: Validate with Hacs\n\non:\n  push:\n    branches:\n      - master\n\n  pull_request:\n    branches: [\"*\"]\n\n  schedule:\n   "
  },
  {
    "path": ".gitignore",
    "chars": 210,
    "preview": "# 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"
  },
  {
    "path": ".prettierignore",
    "chars": 57,
    "preview": "*.md\n.strict-typing\nazure-*.yml\ndocs/source/_templates/*\n"
  },
  {
    "path": ".pylintrc",
    "chars": 1561,
    "preview": "[MESSAGES CONTROL]\n# PyLint message control settings\n# Reasons disabled:\n# format - handled by black\n# locally-disabled "
  },
  {
    "path": ".ruff.toml",
    "chars": 1926,
    "preview": "# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml\n\ntarget-version ="
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 72,
    "preview": "{\n  \"recommendations\": [\"esbenp.prettier-vscode\", \"ms-python.python\"]\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 1012,
    "preview": "{\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"config"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 447,
    "preview": "{\n  //\"editor.formatOnSave\": true\n  \"[python]\": {\n    \"editor.defaultFormatter\": \"ms-python.black-formatter\",\n    \"edito"
  },
  {
    "path": ".vscode/tasks.json",
    "chars": 1186,
    "preview": "{\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"Run Home Assistant on port 8123\",\n            \""
  },
  {
    "path": "Dockerfile.dev",
    "chars": 1279,
    "preview": "FROM mcr.microsoft.com/devcontainers/python:1-3.13\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# Uninstall pre-install"
  },
  {
    "path": "LICENSE",
    "chars": 11324,
    "preview": "Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licens"
  },
  {
    "path": "README.md",
    "chars": 28320,
    "preview": "[![](https://img.shields.io/github/release/ollo69/ha-samsungtv-smart/all.svg?style=for-the-badge)](https://github.com/ol"
  },
  {
    "path": "config/configuration.yaml",
    "chars": 301,
    "preview": "# Loads default set of integrations. Do not remove.\ndefault_config:\n\n# Load frontend themes from the themes folder\nfront"
  },
  {
    "path": "custom_components/__init__.py",
    "chars": 32,
    "preview": "\"\"\"Custom components module.\"\"\"\n"
  },
  {
    "path": "custom_components/samsungtv_smart/__init__.py",
    "chars": 22684,
    "preview": "\"\"\"The samsungtv_smart integration.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nim"
  },
  {
    "path": "custom_components/samsungtv_smart/api/__init__.py",
    "chars": 41,
    "preview": "\"\"\"SamsungTV Smart TV WS API library.\"\"\"\n"
  },
  {
    "path": "custom_components/samsungtv_smart/api/samsungcast.py",
    "chars": 2357,
    "preview": "\"\"\"Smartthings TV integration cast tube.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport xml.etree.Element"
  },
  {
    "path": "custom_components/samsungtv_smart/api/samsungws.py",
    "chars": 43441,
    "preview": "\"\"\"\nSamsungTVWS - Samsung Smart TV WS API wrapper\n\nCopyright (C) 2019 Xchwarze\nCopyright (C) 2020 Ollo69\n\n    This libra"
  },
  {
    "path": "custom_components/samsungtv_smart/api/shortcuts.py",
    "chars": 2667,
    "preview": "\"\"\"\nSamsungTVWS - Samsung Smart TV WS API wrapper\n\nCopyright (C) 2019 Xchwarze\n\n    This library is free software; you c"
  },
  {
    "path": "custom_components/samsungtv_smart/api/smartthings.py",
    "chars": 18124,
    "preview": "\"\"\"SmartThings TV integration.\"\"\"\n\nfrom __future__ import annotations\n\nfrom asyncio import TimeoutError as AsyncTimeoutE"
  },
  {
    "path": "custom_components/samsungtv_smart/api/upnp.py",
    "chars": 4241,
    "preview": "\"\"\"Smartthings TV integration UPnP implementation.\"\"\"\n\nimport logging\nfrom typing import Optional\nimport xml.etree.Eleme"
  },
  {
    "path": "custom_components/samsungtv_smart/config_flow.py",
    "chars": 31768,
    "preview": "\"\"\"Config flow for Samsung TV.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom numbers import Number\nimport "
  },
  {
    "path": "custom_components/samsungtv_smart/const.py",
    "chars": 4089,
    "preview": "\"\"\"Constants for the samsungtv_smart integration.\"\"\"\n\nfrom enum import Enum\n\n\nclass AppLoadMethod(Enum):\n    \"\"\"Valid ap"
  },
  {
    "path": "custom_components/samsungtv_smart/diagnostics.py",
    "chars": 3403,
    "preview": "\"\"\"Diagnostics support for Samsung TV Smart.\"\"\"\n\nfrom __future__ import annotations\n\nfrom homeassistant.components.diagn"
  },
  {
    "path": "custom_components/samsungtv_smart/entity.py",
    "chars": 1523,
    "preview": "\"\"\"Base SamsungTV Entity.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom homeassistant.const impor"
  },
  {
    "path": "custom_components/samsungtv_smart/logo.py",
    "chars": 13622,
    "preview": "\"\"\"Logo implementation for SamsungTV Smart.\"\"\"\n\nimport asyncio\nfrom datetime import datetime, timedelta, timezone\nfrom e"
  },
  {
    "path": "custom_components/samsungtv_smart/logo_paths.json",
    "chars": 75272,
    "preview": "{\"fuji tv\":\"/yS5UJjsSdZXML0YikWTYYHLPKhQ.png\",\"abc\":\"/kMvN5R8g6L0SY5r9YZw9foYGQr0.png\",\"bbc three\":\"/ex369Frq6w0PaQsVofp"
  },
  {
    "path": "custom_components/samsungtv_smart/manifest.json",
    "chars": 571,
    "preview": "{\n  \"domain\": \"samsungtv_smart\",\n  \"name\": \"SamsungTV Smart\",\n  \"after_dependencies\": [\"smartthings\"],\n  \"codeowners\": ["
  },
  {
    "path": "custom_components/samsungtv_smart/media_player.py",
    "chars": 62086,
    "preview": "\"\"\"Support for interface with an Samsung TV.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc"
  },
  {
    "path": "custom_components/samsungtv_smart/remote.py",
    "chars": 4108,
    "preview": "\"\"\"Support for the SamsungTV remote.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom d"
  },
  {
    "path": "custom_components/samsungtv_smart/services.yaml",
    "chars": 914,
    "preview": "select_picture_mode:\n  name: Select Picture Mode\n  description: Send to samsung TV the command to change picture mode.\n "
  },
  {
    "path": "custom_components/samsungtv_smart/translations/en.json",
    "chars": 5850,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"This Samsung TV is already configured.\",\n      \"already_in_p"
  },
  {
    "path": "custom_components/samsungtv_smart/translations/hu.json",
    "chars": 5437,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Ez a Samsung TV már konfigurálva van.\",\n      \"already_in_pr"
  },
  {
    "path": "custom_components/samsungtv_smart/translations/it.json",
    "chars": 6349,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Questo Samsung TV \\u00e8 gi\\u00e0 configurato.\",\n      \"alre"
  },
  {
    "path": "custom_components/samsungtv_smart/translations/pt-BR.json",
    "chars": 3979,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"Essa TV Samsung já está configurada.\",\n      \"already_in_pro"
  },
  {
    "path": "docs/App_list.md",
    "chars": 2732,
    "preview": "# HomeAssistant - SamsungTV Smart Component\n\n***app_list guide***\n---------------\n\n**Note:** Although this is an optiona"
  },
  {
    "path": "docs/Key_chaining.md",
    "chars": 1634,
    "preview": "# HomeAssistant - SamsungTV Smart Component\n\n***Key Chaining Patterns***\n---------------\n\n**Note:** If SmartThings API w"
  },
  {
    "path": "docs/Key_codes.md",
    "chars": 6892,
    "preview": "# HomeAssistant - SamsungTV Smart Component\n\n***Key Codes***\n---------------\nIf [SmartThings API](https://github.com/oll"
  },
  {
    "path": "docs/Smartthings.md",
    "chars": 3980,
    "preview": "# HomeAssistant - SamsungTV Smart Integration\n\n## ***Enable SmartThings*** - Setup instructions\n\n### SmartThings authent"
  },
  {
    "path": "hacs.json",
    "chars": 151,
    "preview": "{\n  \"name\": \"SamsungTV Smart\",\n  \"content_in_root\": false,\n  \"zip_release\": true,\n  \"filename\": \"samsungtv_smart.zip\",\n "
  },
  {
    "path": "info.md",
    "chars": 4046,
    "preview": "\n# HomeAssistant - SamsungTV Smart Component\n\nThis is a custom component to allow control of SamsungTV devices in [HomeA"
  },
  {
    "path": "requirements.txt",
    "chars": 239,
    "preview": "# 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\nflake"
  },
  {
    "path": "requirements_test.txt",
    "chars": 256,
    "preview": "# Strictly for tests\npytest==8.3.5\n#pytest-cov==2.9.0\n#pytest-homeassistant\npytest-homeassistant-custom-component==0.13."
  },
  {
    "path": "script/integration_init",
    "chars": 157,
    "preview": "#!/usr/bin/env bash\n\n# Create empty init in custom components directory\necho \"Init custom components directory\"\ntouch \"$"
  },
  {
    "path": "scripts/develop",
    "chars": 609,
    "preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\n# Create config dir if not present\nif [[ ! -d \"${PWD}/config\" ]]; "
  },
  {
    "path": "scripts/lint",
    "chars": 73,
    "preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\nruff check . --fix\n"
  },
  {
    "path": "scripts/setup",
    "chars": 108,
    "preview": "#!/usr/bin/env bash\n\nset -e\n\ncd \"$(dirname \"$0\")/..\"\n\npython3 -m pip install --requirement requirements.txt\n"
  },
  {
    "path": "setup.cfg",
    "chars": 1251,
    "preview": "[coverage:run]\nsource =\n  custom_components\n\n[coverage:report]\nexclude_lines =\n    pragma: no cover\n    raise NotImpleme"
  },
  {
    "path": "tests/__init__.py",
    "chars": 31,
    "preview": "\"\"\"custom integation tests.\"\"\"\n"
  },
  {
    "path": "tests/conftest.py",
    "chars": 1912,
    "preview": "\"\"\"Global fixtures for integration_blueprint integration.\"\"\"\n\n# Fixtures allow you to replace functions with a Mock obje"
  }
]

About this extraction

This page contains the full source code of the ollo69/ha-samsungtv-smart GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 59 files (381.6 KB), approximately 113.6k tokens, and a symbol index with 358 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!