Skip to content

Samsung TV's not working [Testing fix] #20795

Description

@kdschlosser

*****UPDATED*****
PLEASE READ ALL THE INFORMATION BELOW THERE ARE PARTS THAT HAVE CHANGED

I am moving the conversation that is taking place in #17802 as the current conversation really has nothing to do with the issue that was reported.

If this is not OK then delete this issue. I am the person that is currently updating the code to samsungctl. It has undergone a lot of additions which I do believe the users of Home Assistant will enjoy being able to use.

thee library also now supports ALL Samsung TV's that have the capability of being controlled over a network or WiFi Connection from 2008 to present. This includes the encrypted websocket connections (H (2014) and J (2015) TV's) as well as the SSL websocket connection (latest firmware release).

I have simplified the connection process so that the only things that are needed in thee Home Assistant config file is the host (ip) of the TV. nothing else is needed. the samsungctl library now handles hammering all of the connection specific details. I also added to it a a class that handles loading and saving of config data. I did this to handle the dynamically changing tokens that are used with the encrypted websockets and SSL websockets. the library now also does not stop running if the connection get severed.. (TV gets powered off). I also changed the handling of the power keys to make them function like they should. KEY_POWER = power toggle, KEY_POWERON = power on and KEY_POWEROFF = power off.

I am currently testing the replacement for /homeassistant/components/media_player/samsungtv.py, because I am not familiar with Home Assistant it is better to hammer these problems out in an issue where they can be tested then to open a PR for non functioning code and end up with a very messy commit history.

PLEASE READ

This is a replacement for the /homeassistant/components/media_player/samsungtv.py file
please do not edit the code and then report an issue. only report problems relating to this code. also do not provide vague errors. like it says config error.. I am willing to bet the error stats more then "config error". I need a copy of the error message in it's entirety. not little bits and pieces. because of the nature of Home Assistant you may need to scroll back in the logs and read for a bit to make sure that you have grabbed all of the relevant log messages.

If you state anything about "custom_components" or if i see that in any of the errors I am not going to answer you. this is because you have modified the code. and it is a pretty good chance that your modification is what is causing the problem.

If you do not do the above then I have no way of helping to solve any issues.

config files for the TV are going to be saved in a directory called samsung_tv that is located in the Home Assistant config folder. the config files for the TVs are going to be named using the IP address you supply in the Home Assistant config yaml file.

HOME ASSISTANT CONFIG FILE
This is the only thing that needs to be added to your hass configuration.yaml file. you will not need to specify any kind of a host. or mac or name. nothing. The whole system will dynamically add TV's as it finds them (of coarse with your permission). if a TV is found you will get a notification in the hass UI. when you open this notification you will be prompted to change the display name, the mac address and the description. if you do not wish to change them simply click on the accept button. You may need to refresh the page once the device is added for it to show up. If you have more then a single TV you DO NOT need to add multiple entries into the hass config file. You only need to add the below code a single time.

If you do not want to add a TV to hass (not sure why you wouldn't) simply ignore the notification. it will disappear after 60 seconds. if you change your mind afterwards or you simply miss the timeout you will need to go into thee samsung_tv config directory and delete the file that has an extension of .noinclude.

media_player:
  - platform: samsungtv

the config file will only get saved once there is a successful connection to the TV made. I have changed about the detection mechanism for the 4 different types of connections. I am hoping this is going to be more of a dock solid mechanism of detecting the connection type. IF you have an issue detecting your TV let me know. There are some tasks I will have you do in order to get it to detect the TV properly. I do not own every Samsung TV made so there is no way to test it on my end. so it is up to you guys to follow the bug reporting directions as outline above.

Here is the updated code.

I WILL NOT MAKE CHANGES AND MAKE ANOTHER POST FOR IT. I WILL UPDATE THE CODE BELOW WITH ANY NEW CHANGES AND INFORM YOU THAT THE CODE HAS CHANGED

click to expand

"""
Support for interface with an Samsung TV.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.samsungtv/
"""
import asyncio
from datetime import timedelta
import logging
import threading
import os
import uuid

import voluptuous as vol

from homeassistant.components.media_player import (
    MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
    SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF,
    SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
    SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import (
    CONF_NAME,
    STATE_OFF,
    STATE_ON
)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import dt as dt_util


REQUIREMENTS = [
     'https://github.com/kdschlosser/'
     'samsungctl/archive/develop.zip#samsungctl==0.8.64b'
]

SAMSUNG_CONFIG_PATH = 'samsung_tv'

ICON_TV = 'mdi:television'
ICON_TV_OFF = 'mdi:television-off'
ICON_COMPONENT = 'mdi:video-input-component'
ICON_HDMI = 'mdi:video-input-hdmi'
ICON_SVIDEO = 'mdi:video-input-svideo'
ICON_USB = 'mdi:usb'
ICON_PC = 'mdi:console'
ICON_DLNA = 'mdi:dlna'
ICON_AV = 'mdi:audio-video'
ICON_YOUTUBE = 'mdi:youtube'
ICON_HULU = 'mdi:hulu'
ICON_NETFLIX = 'mdi:netflix'
ICON_PLEX = 'mdi:plex'
ICON_SPOTIFY = 'mdi:spotify'
ICON_AMAZON = 'mdi:amazon'
ICON_PLAYSTATION = 'mdi:playstation'
ICON_UNKNOWN = 'mdi:help'

_LOGGER = logging.getLogger(__name__)

CONF_DESCRIPTION = 'description'
CONF_ADD = 'add_tv'

KEY_PRESS_TIMEOUT = 1.2
KNOWN_DEVICES_KEY = 'samsungtv_known_devices'

SUPPORT_SAMSUNGTV = (
    SUPPORT_PAUSE |
    SUPPORT_VOLUME_STEP |
    SUPPORT_VOLUME_MUTE |
    SUPPORT_PREVIOUS_TRACK |
    SUPPORT_NEXT_TRACK |
    SUPPORT_TURN_OFF |
    SUPPORT_PLAY |
    SUPPORT_PLAY_MEDIA |
    SUPPORT_VOLUME_SET |
    SUPPORT_SELECT_SOURCE
)


SAMSUNG_TV_SCHEMA = vol.Schema({
    vol.Optional(CONF_NAME): cv.string,
    vol.Optional(CONF_DESCRIPTION): cv.string,
    vol.Optional(CONF_ADD): cv.boolean,
})

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})

_CONFIGURING = {}


def setup_platform(hass, config, add_entities, _=None):
    """Set up the Samsung TV platform."""

    config_path = hass.config.path(SAMSUNG_CONFIG_PATH)

    if not os.path.exists(config_path):
        os.mkdir(config_path)

    known_devices = hass.data.get(KNOWN_DEVICES_KEY, set())
    hass.data[KNOWN_DEVICES_KEY] = known_devices

    import samsungctl
    from samsungctl.upnp.discover import auto_discover

    config_files = list(
        os.path.join(config_path, file) for file in os.listdir(config_path)
            if file.endswith('config')
    )

    def callback(found_config):
        if found_config.uuid in known_devices:
            return

        _LOGGER.debug(str(found_config))
        known_devices.add(found_config.uuid)

        no_include = os.path.join(
            config_path,
            found_config.uuid + '.noinclude'
        )

        if os.path.exists(no_include):
            return

        if found_config.uuid not in _CONFIGURING:
            add_device(found_config, hass, config_path, add_entities)

    auto_discover.register_callback(callback)
    entities = []
    configs = []

    for config_file in config_files:
        _LOGGER.debug(config_file)
        samsung_config = samsungctl.Config.load(config_file)
        known_devices.add(samsung_config.uuid)
        configs += [samsung_config]

    auto_discover.start()

    for samsung_config in configs:
        entities += [SamsungTVDevice(samsung_config)]
    add_entities(entities)


def add_device(samsung_config, hass, config_path, add_entities):
    model = samsung_config.model
    uuid = samsung_config.uuid

    event = threading.Event()

    def samsung_configuration_callback(data):
        """Handle the entry of user PIN."""
        display_name = data.get('display_name')
        description = data.get('description')
        mac = data.get('mac')

        if display_name is None:
            display_name = samsung_config.display_name
        if description is None:
            description = samsung_config.description
        if mac is None:
            mac = samsung_config.mac

        samsung_config.display_name = display_name
        samsung_config.description = description
        samsung_config.mac = mac

        samsung_config.path = os.path.join(
            config_path,
            samsung_config.uuid + '.config'
        )

        hass.components.configurator.request_done(_CONFIGURING.pop(uuid))

        if samsung_config.method == 'encrypted':
            request_configuration(samsung_config, hass, add_entities)
        else:
            add_entities([SamsungTVDevice(samsung_config)])

        event.set()

    def do():
        event.wait(600.0)
        if not event.isSet():
            path = os.path.join(
                config_path,
                samsung_config.uuid + '.noinclude'
            )

            with open(path, 'w') as f:
                f.write('')

            hass.components.configurator.request_done(_CONFIGURING.pop(uuid))

    t = threading.Thread(target=do)
    t.daemon = True
    t.start()

    _CONFIGURING[uuid] = hass.components.configurator.request_config(
        model,
        samsung_configuration_callback,
        description='New TV discovered, would you like to add the TV?',
        description_image="/static/images/smart-tv.png",
        submit_caption="Accept",
        fields=[
            dict(
                id='display_name',
                name='Name: ' + samsung_config.display_name,
                type=''
            ),
            dict(
                id='description',
                name='Description: ' + samsung_config.description,
                type=''
            ),
            dict(
                id='mac',
                name='MAC Address : ' + str(samsung_config.mac),
                type=''
            )
        ]
    )


def request_configuration(samsung_config, hass, add_entities):
    """Request configuration steps from the user."""

    configurator = hass.components.configurator

    import samsungctl

    pin = []
    count = 0
    event = threading.Event()

    def samsung_configuration_callback(data):
        """Handle the entry of user PIN."""
        pin.append(data.get('pin'))
        event.set()

    def get_pin():
        global count

        if samsung_config.uuid in _CONFIGURING:
            count += 1
            event.clear()
            del pin[:]

            configurator.notify_errors(
                _CONFIGURING[samsung_config.uuid],
                "Failed to register, please try again."
            )
        else:
            _CONFIGURING[samsung_config.uuid] = configurator.request_config(
                samsung_config.display_name,
                samsung_configuration_callback,
                description='Enter the Pin shown on your Samsung TV.',
                description_image="/static/images/smart-tv.png",
                submit_caption="Confirm",
                fields=[{'id': 'pin', 'name': 'Enter the pin', 'type': ''}]
            )

        event.wait(60.0)

        if count == 3:
            _LOGGER.error(
                samsung_config.display_name + " TV: Pin entry failed"
            )
            return False
        elif not event.isSet():
            return None

        return pin[0]

    samsung_config.get_pin = get_pin

    def do():
        global count

        try:
            _ = samsungctl.Remote(samsung_config)
            add_entities([SamsungTVDevice(samsung_config)])
        except:
            pass

        hass.components.configurator.request_done(_CONFIGURING.pop(uuid))

    t = threading.Thread(target=do)
    t.daemon = True
    t.start()


class SamsungTVDevice(MediaPlayerDevice):
    """Representation of a Samsung TV."""

    def __init__(self, config):
        """Initialize the Samsung device."""
        from samsungctl import exceptions
        from samsungctl import Remote

        # Save a reference to the imported classes
        self._exceptions_class = exceptions
        self._remote_class = Remote
        self._config = config
        self._mac = self._config.mac
        self._uuid = self._config.uuid
        self._playing = True
        self._state = None
        self._remote = None
        self._key_source = False
        self._mute = False
        self._sources = []
        self._source = ''
        self._volume = 0.0
        self._entity_image = None
        self._tv_image = None

        if self._config.method == 'websocket':
            self._has_apps = True
        else:
            self._has_apps = False
        self._icon = ICON_TV_OFF

        self._supported_features = SUPPORT_SAMSUNGTV
        if self._config.method != 'legacy':
            self._supported_features |= SUPPORT_TURN_ON

        # Mark the end of a shutdown command (need to wait 15 seconds before
        # sending the next command to avoid turning the TV back ON).
        self._end_of_power_off = None

        # Mark the end of the TV powering on.need to wait 20 seconds before
        # sending any commands.
        self._end_of_power_on = None
        # Generate a configuration for the Samsung library

        self._remote = self._remote_class(self._config)

    def update(self):
        """Update state of device."""
        if self._power_off_in_progress():
            _LOGGER.debug(
                self._config.display_name + ' TV: Powering Off'
            )
            self._state = STATE_OFF
            self._icon = ICON_TV_OFF
            # self._entity_image = self._tv_image

        elif self._power_on_in_progress():
            _LOGGER.debug(
                self._config.display_name + ' TV: Powering On'
            )
            self._state = STATE_OFF
            self._icon = ICON_TV_OFF
            # self._entity_image = self._tv_image
        else:
            power = self._remote.power
            if power is True and self._remote.is_connected:
                self._config.save()

                if self._tv_image is None:
                    tv_image = self._remote.icon
                    if tv_image is not None:
                        self._tv_image = tv_image.data
                sources = self._remote.sources
                entity_image = self._tv_image
                source = 'Unknown'

                if sources is None:
                    if self._has_apps:
                        sources = [
                            'TV',
                            'HDMI'
                        ]

                        for app in self._remote.applications:

                            if app.is_running and app.is_visible:
                                source = 'APP: ' +app.name
                                entity_image = app.icon

                            sources += ['APP: ' + app.name]

                        self._sources = sources
                        self._source = source
                        self._entity_image = entity_image
                    else:
                        self._sources = [
                            'Source',
                            'Component 1',
                            'Component 2',
                            'AV 1',
                            'AV 2',
                            'AV 3',
                            'S Video 1',
                            'S Video 2',
                            'S Video 3',
                            'HDMI',
                            'HDMI 1',
                            'HDMI 2',
                            'HDMI 3',
                            'HDMI 4',
                            'FM-Radio',
                            'DVI',
                            'DVR',
                            'TV',
                            'Analog TV',
                            'Digital TV'
                        ]
                        self._key_source = True
                else:
                    new_sources = []
                    for src in sources:
                        if src.is_active:
                            if src.label != src.name:
                                source = src.label + ':' + src.name
                            else:
                                source = src.name

                        if src.name != src.label:
                            new_sources += [src.label + ':' + src.name]
                        else:
                            new_sources += [src.name]

                    self._key_source = False
                    self._sources = new_sources[:]

                self._source = source
                # self._entity_image = entity_image

                if self._source.upper().endswith('TV'):
                    self._icon = ICON_TV
                elif self._source.upper().endswith('USB'):
                    self._icon = ICON_USB
                elif self._source.upper().endswith('PC'):
                    self._icon = ICON_PC
                elif self._source.upper().endswith('DLNA'):
                    self._icon = ICON_DLNA

                elif 'S VIDEO' in self._source.upper():
                    self._icon = ICON_SVIDEO
                elif 'COMPONENT' in self._source.upper():
                    self._icon = ICON_COMPONENT
                elif 'AV' in self._source.upper():
                    self._icon = ICON_AV
                elif 'HDMI' in self._source.upper():
                    self._icon = ICON_HDMI
                elif 'YOUTUBE' in self._source.upper():
                    self._icon = ICON_YOUTUBE
                elif 'HULU' in self._source.upper():
                    self._icon = ICON_HULU
                elif 'NETFLIX' in self._source.upper():
                    self._icon = ICON_NETFLIX
                elif 'PLEX' in self._source.upper():
                    self._icon = ICON_PLEX
                elif 'SPOTIFY' in self._source.upper():
                    self._icon = ICON_SPOTIFY
                elif 'AMAZON' in self._source.upper():
                    self._icon = ICON_AMAZON
                elif 'PLAYSTATION' in self._source.upper():
                    self._icon = ICON_PLAYSTATION
                else:
                    self._icon = ICON_UNKNOWN

                volume = self._remote.volume
                _LOGGER.debug(
                    self._config.display_name + ' TV: Volume = ' + str(volume)
                )
                if volume is not None:
                    self._volume = volume / 100.0

                mute = self._remote.mute
                _LOGGER.debug(
                    self._config.display_name + ' TV: Mute = ' + str(mute)
                )
                if mute is None:
                    self._mute = False
                else:
                    self._mute = mute

                _LOGGER.debug(
                    self._config.display_name + ' TV: Power is On'
                )
                self._state = STATE_ON
            else:
                _LOGGER.debug(
                    self._config.display_name + ' TV: Power is Off'
                )
                # self._entity_image = self._tv_image
                self._icon = ICON_TV_OFF
                self._state = STATE_OFF

    def send_key(self, key):
        """Send a key to the tv and handles exceptions."""
        if self._power_off_in_progress():
            _LOGGER.info(
                self._config.display_name +
                " TV: powering off, not sending command: %s",
                key
            )
            return

        elif self._power_on_in_progress():
            _LOGGER.info(
                self._config.display_name +
                " TV: powering on, not sending command: %s",
                key
            )
            return

        if self._state == STATE_OFF:
            _LOGGER.info(
                self._config.display_name +
                " TV: powered off, not sending command: %s",
                key
            )
            return

        self._remote.control(key)

    @property
    def icon(self):
        """Return the icon to use in the frontend, if any."""
        return self._icon

    @property
    def entity_picture(self):
        """Return the entity picture to use in the frontend, if any."""
        return self._entity_image

    @property
    def unique_id(self) -> str:
        """Return the unique ID of the device."""
        return '{' + self._config.uuid + '}'

    @property
    def name(self):
        """Return the name of the device."""
        return self._config.display_name

    @property
    def state(self):
        """Return the state of the device."""
        return self._state

    @property
    def supported_features(self):
        """Flag media player features that are supported."""
        return self._supported_features

    def select_source(self, source):
        """Select input source."""
        if self._key_source:
            if source == 'Analog TV':
                source = 'ANTENA'

            elif source == 'Digital TV':
                source = 'DTV'

            source = source.upper().replace('-', '_').replace(' ', '')
            source = 'KEY_' + source
            _LOGGER.debug(
                self._config.display_name + ' TV: changing source to ' + source
            )
            self.send_key(source)
        else:
            if 'APP' in source:
                app_name = source.rsplit(':', 1)[-1]
                app = self._remote.get_application(app_name)

                if app is not None:
                    app.run()

            if ':' in source:
                source = source.rsplit(':', 1)[-1]
            _LOGGER.debug(
                self._config.display_name + ' TV: changing source to ' + source
            )
            self._remote.source = source

    @property
    def source(self):
        """Name of the current input source."""
        return self._source

    @property
    def source_list(self):
        """List of available input sources."""
        return self._sources

    def volume_up(self):
        """Volume up the media player."""
        self.send_key('KEY_VOLUP')

    def volume_down(self):
        """Volume down media player."""
        self.send_key('KEY_VOLDOWN')

    @property
    def volume_level(self):
        """Volume level of the media player scalar volume. 0.0-1.0."""
        return self._volume

    def set_volume_level(self, volume):
        """Set volume level, convert scalar volume. 0.0-1.0 to percent 0-100"""
        self._remote.volume = int(volume * 100)

    def mute_volume(self, mute):
        """Send mute command."""
        self._remote.mute = mute

    @property
    def is_volume_muted(self):
        """Boolean if volume is currently muted."""
        return self._mute

    def media_play_pause(self):
        """Simulate play pause media player."""
        if self._playing:
            self.media_pause()
        else:
            self.media_play()

    def media_play(self):
        """Send play command."""
        self._playing = True
        self.send_key('KEY_PLAY')

    def media_pause(self):
        """Send media pause command to media player."""
        self._playing = False
        self.send_key('KEY_PAUSE')

    def media_next_track(self):
        """Send next track command."""
        self.send_key('KEY_FF')

    def media_previous_track(self):
        """Send the previous track command."""
        self.send_key('KEY_REWIND')

    async def async_play_media(self, media_type, media_id, **kwargs):
        """Support changing a channel."""
        if media_type != MEDIA_TYPE_CHANNEL:
            _LOGGER.error(
                self._config.display_name + ' TV: Unsupported media type'
            )
            return

        # media_id should only be a channel number
        try:
            cv.positive_int(media_id)
        except vol.Invalid:
            _LOGGER.error(
                self._config.display_name +
                ' TV: Media ID must be positive integer'
            )
            return

        for digit in media_id:
            await self.hass.async_add_job(self.send_key, 'KEY_' + digit)
            await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop)

    @property
    def app_id(self):
        """ID of the current running app."""
        return None

    @property
    def app_name(self):
        """Name of the current running app."""
        return None

    def turn_on(self):
        """Turn the media player on."""

        if self._power_on_in_progress():
            return

        if self._config.mac:
            self._end_of_power_on = dt_util.utcnow() + timedelta(seconds=20)

            if self._power_off_in_progress():
                self._end_of_power_on += (
                    dt_util.utcnow() - self._end_of_power_off
                )

            def do():
                _LOGGER.debug(
                    self._config.display_name + ' TV: Power on process started'
                )
                event = threading.Event()
                while self._power_off_in_progress():
                    event.wait(0.5)

                self._remote.power = True

            t = threading.Thread(target=do)
            t.daemon = True
            t.start()
        elif self._config.method != 'legacy':
            _LOGGER.info(
                self._config.display_name +
                " TV: There was a problem detecting the TV's MAC address, "
                "you will have to update the MAC address in the Home "
                "Assistant config file manually."
            )

        else:
            _LOGGER.info(
                self._config.display_name +
                " TV: Legacy TV's (2008 - 2013) do not support "
                "being powered on remotely."
            )

    def _power_on_in_progress(self):
        return (
            self._end_of_power_on is not None and
            self._end_of_power_on > dt_util.utcnow()
        )

    def turn_off(self):
        """Turn off media player."""

        if self._power_off_in_progress():
            return

        self._end_of_power_off = dt_util.utcnow() + timedelta(seconds=15)

        if self._power_on_in_progress():
            self._end_of_power_off += (
                dt_util.utcnow() - self._end_of_power_on
            )

        def do():
            _LOGGER.debug(
                self._config.display_name + ' TV: Power off process started'
            )
            event = threading.Event()
            while self._power_on_in_progress():
                event.wait(0.5)

            self._remote.power = False

        t = threading.Thread(target=do)
        t.daemon = True
        t.start()

    def _power_off_in_progress(self):
        return (
            self._end_of_power_off is not None and
            self._end_of_power_off > dt_util.utcnow()
        )

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions