Пример #1
0
class NUTSensor(CoordinatorEntity, SensorEntity):
    """Representation of a sensor entity for NUT status values."""
    def __init__(
        self,
        coordinator: DataUpdateCoordinator,
        sensor_description: SensorEntityDescription,
        data: PyNUTData,
        unique_id: str,
    ) -> None:
        """Initialize the sensor."""
        super().__init__(coordinator)
        self.entity_description = sensor_description

        device_name = data.name.title()
        self._attr_name = f"{device_name} {sensor_description.name}"
        self._attr_unique_id = f"{unique_id}_{sensor_description.key}"
        self._attr_device_info = DeviceInfo(
            identifiers={(DOMAIN, unique_id)},
            name=device_name,
        )
        self._attr_device_info.update(data.device_info)

    @property
    def native_value(self):
        """Return entity state from ups."""
        status = self.coordinator.data
        if self.entity_description.key == KEY_STATUS_DISPLAY:
            return _format_display_state(status)
        return status.get(self.entity_description.key)
Пример #2
0
class SamsungTVDevice(MediaPlayerEntity):
    """Representation of a Samsung TV."""
    def __init__(self, config, entry_id, session: ClientSession, token_file,
                 logo_file):
        """Initialize the Samsung device."""

        self._entry_id = entry_id
        self._session = session
        self._host = config[CONF_HOST]
        self._mac = config.get(CONF_MAC)

        # Set entity attributes
        self._attr_name = config.get(CONF_NAME, self._host)
        self._attr_unique_id = config.get(CONF_ID, entry_id)
        self._attr_icon = "mdi:television"
        self._attr_device_class = DEVICE_CLASS_TV
        self._attr_supported_features = SUPPORT_SAMSUNGTV_SMART
        self._attr_media_title = None
        self._attr_media_image_url = None

        self._attr_device_info = DeviceInfo(
            identifiers={(DOMAIN, self._attr_unique_id)},
            name=self._attr_name,
            manufacturer="Samsung Electronics",
        )
        self._attr_device_info.update(
            self._get_add_dev_info(
                config.get(CONF_DEVICE_MODEL),
                config.get(CONF_DEVICE_NAME),
                config.get(CONF_DEVICE_OS),
                self._mac,
            ))

        # Save a reference to the imported config
        self._broadcast = config.get(CONF_BROADCAST_ADDRESS)

        # load sources list
        self._default_source_used = False
        source_list = SamsungTVDevice._load_param_list(
            config.get(CONF_SOURCE_LIST, {}))
        if not source_list:
            source_list = DEFAULT_SOURCE_LIST
            self._default_source_used = True
        self._source_list = source_list

        # load apps list
        self._dump_apps = True
        app_list = SamsungTVDevice._load_param_list(config.get(CONF_APP_LIST))
        if app_list is not None:
            double_list = SamsungTVDevice._split_app_list(app_list, "/")
            self._app_list = double_list["app"]
            self._app_list_ST = double_list["appST"]
        else:
            self._app_list = None
            self._app_list_ST = None

        # load channels list
        self._channel_list = SamsungTVDevice._load_param_list(
            config.get(CONF_CHANNEL_LIST))

        self._source = None
        self._running_app = None
        # Assume that the TV is not muted and volume is 0
        self._muted = False
        self._volume = 0
        # Assume that the TV is in Play mode
        self._playing = True
        self._state = STATE_UNAVAILABLE
        # 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
        self._power_on_detected = None
        self._set_update_forced = False
        self._update_forced_time = None
        self._fake_on = None
        self._delayed_set_source = None
        self._delayed_set_source_time = None

        ws_name = config.get(CONF_WS_NAME, self._attr_name)
        ws_port = config.get(CONF_PORT, DEFAULT_PORT)
        ws_timeout = config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT)
        self._ws = SamsungTVWS(
            name=
            f"{WS_PREFIX} {ws_name}",  # this is the name shown in the TV list of external device.
            host=self._host,
            port=ws_port,
            timeout=ws_timeout,
            key_press_delay=KEYPRESS_DEFAULT_DELAY,
            token_file=token_file,
            app_list=self._app_list,
        )

        self._upnp = upnp(host=self._host, session=session)

        self._st = None
        api_key = config.get(CONF_API_KEY)
        device_id = config.get(CONF_DEVICE_ID)
        if api_key and device_id:
            self._st = SmartThingsTV(
                api_key=api_key,
                device_id=device_id,
                use_channel_info=True,
                session=session,
            )
            self._attr_supported_features |= SUPPORT_SELECT_SOUND_MODE

        self._st_error_count = 0
        self._setvolumebyst = False

        self._logo_option = LOGO_OPTION_DEFAULT[0]
        self._logo = Logo(
            logo_option=self._logo_option,
            logo_file_download=logo_file,
            session=session,
        )

    @staticmethod
    def _get_add_dev_info(dev_model, dev_name, dev_os, dev_mac):
        """Get additional device information."""
        model = dev_model or "Samsung TV"
        if dev_name:
            model = f"{model} ({dev_name})"

        dev_info = DeviceInfo(model=model)
        if dev_os:
            dev_info[ATTR_SW_VERSION] = dev_os
        if dev_mac:
            dev_info["connections"] = {(CONNECTION_NETWORK_MAC, dev_mac)}

        return dict(dev_info)

    @staticmethod
    def _load_param_list(src_list):
        """Load 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

    @staticmethod
    def _split_app_list(app_list, sep=ST_APP_SEPARATOR):
        """Split the application list for standard and SmartThings."""
        retval = {"app": {}, "appST": {}}

        for app_name, value in app_list.items():
            value_split = value.split(sep, 1)
            app_id = value_split[0]
            if len(value_split) == 1:
                st_app_id = STD_APP_LIST.get(app_id, app_id) or app_id
            else:
                st_app_id = value_split[1]
            retval["app"][app_name] = app_id
            retval["appST"][app_name] = st_app_id

        return retval

    def _get_option(self, param, default=None):
        """Get option from entity configuration."""
        entry_id = self.hass.data.get(DOMAIN, {}).get(self._entry_id)
        if not entry_id:
            return default
        option = entry_id[DATA_OPTIONS].get(param)
        return default if option is None else option

    def _power_off_in_progress(self):
        """Check if a power off request is in progress."""
        return (self._end_of_power_off is not None
                and self._end_of_power_off > dt_util.utcnow())

    def _update_forced(self):
        """Check if a forced update is required."""
        if self._set_update_forced:
            self._update_forced_time = datetime.now()
            self._power_on_detected = datetime.min
            self._set_update_forced = False
            return False

        if not self._update_forced_time:
            return False

        call_time = datetime.now()
        difference = (call_time - self._update_forced_time).total_seconds()
        if difference >= 10:
            self._update_forced_time = None
            return False
        return True

    def _delay_power_on(self, result):
        """Manage delay for power on status."""
        if result and self._state == STATE_OFF:

            power_on_delay = self._get_option(CONF_POWER_ON_DELAY,
                                              DEFAULT_POWER_ON_DELAY)

            if power_on_delay > 0:
                if not self._power_on_detected:
                    self._power_on_detected = datetime.now()
                difference = (datetime.now() -
                              self._power_on_detected).total_seconds()
                if difference < power_on_delay:
                    return False
        else:
            if self._ws.artmode_status == ArtModeStatus.On:
                self._power_on_detected = datetime.min
            else:
                self._power_on_detected = None

        return result

    async def _update_volume_info(self):
        """Update the volume info."""
        if self._state == STATE_ON:

            # if self._st and self._setvolumebyst:
            # self._volume = self._st.volume
            # self._muted = self._st.muted
            # return

            self._volume = int(await self._upnp.async_get_volume()) / 100
            self._muted = await self._upnp.async_get_mute()

    def _get_external_entity_status(self):
        """Get status from external binary sensor."""
        ext_entity = self._get_option(CONF_EXT_POWER_ENTITY)
        if not ext_entity:
            return True
        ext_state = self.hass.states.get(ext_entity)
        if not ext_state:
            return True
        return ext_state.state == STATE_ON

    def _ping_device(self):
        """Ping TV with WS and others method to check power status."""

        result = self._ws.ping_device()
        if result and self._st:
            use_st_status = self._get_option(CONF_USE_ST_STATUS_INFO, True)
            if (self._st.state == STStatus.STATE_OFF
                    and self._st.prev_state != STStatus.STATE_OFF
                    and self._state == STATE_ON and use_st_status):
                result = False

        if result:
            result = self._get_external_entity_status()

        if result:
            self._ws.start_client()
            self._ws.get_running_app()
            if (self._ws.artmode_status == ArtModeStatus.On
                    or self._ws.artmode_status == ArtModeStatus.Unavailable):
                result = False
        else:
            self._ws.stop_client()

        return result

    async def _get_running_app(self):
        """Retrieve list of running apps."""

        if self._app_list is not None:

            for app, app_id in self._app_list.items():
                if self._ws.running_app:
                    if app_id == self._ws.running_app:
                        self._running_app = app
                        return
                if self._st and self._st.channel_name != "":
                    st_app_id = self._app_list_ST.get(app, "")
                    if st_app_id == self._st.channel_name:
                        self._running_app = app
                        return

        self._running_app = DEFAULT_APP

    def _get_st_sources(self):
        """Get sources from SmartThings."""
        if self._state != STATE_ON or not self._st:
            _LOGGER.debug(
                "Samsung TV is OFF or SmartThings not configured, _get_st_sources not executed"
            )
            return

        st_source_list = {}
        source_list = self._st.source_list
        if source_list:

            def get_next_name(index):
                if index >= len(source_list):
                    return ""
                next_input = source_list[index]
                if not (next_input.upper() in ["DIGITALTV", "TV"]
                        or next_input.startswith("HDMI")):
                    return next_input
                return ""

            for i in range(len(source_list)):
                try:
                    # SmartThings source list is an array that may contain the input or the assigned name,
                    # if we found a name that is not an input we use it as input name
                    input_name = source_list[i]
                    is_tv = input_name.upper() in ["DIGITALTV", "TV"]
                    is_hdmi = input_name.startswith("HDMI")
                    if is_tv or is_hdmi:
                        input_type = "ST_TV" if is_tv else "ST_" + input_name
                        if input_type in st_source_list.values():
                            continue

                        name = self._st.get_source_name(input_name)
                        if not name:
                            name = get_next_name(i + 1)
                        st_source_list[name or input_name] = input_type

                except Exception:
                    pass

        if len(st_source_list) > 0:
            _LOGGER.info("Samsung TV: loaded sources list from SmartThings: " +
                         str(st_source_list))
            self._source_list = st_source_list
            self._default_source_used = False

    @Throttle(MIN_TIME_BETWEEN_APP_SCANS)
    def _gen_installed_app_list(self, **kwargs):
        """Get apps installed on TV."""

        if self._dump_apps:
            self._dump_apps = self._get_option(CONF_DUMP_APPS, False)

        if not (self._app_list is None or self._dump_apps):
            return

        app_list = self._ws.installed_app
        if not app_list:
            return

        app_load_method = AppLoadMethod(
            self._get_option(CONF_APP_LOAD_METHOD, AppLoadMethod.All.value))

        # app_list is a list of dict
        filtered_app_list = {}
        filtered_app_list_st = {}
        dump_app_list = {}
        for app in app_list.values():
            try:
                app_name = app.app_name
                app_id = app.app_id
                st_app_id = STD_APP_LIST.get(app_id)
                # app_list is automatically created only with apps in hard coded short list (STD_APP_LIST)
                # other available apps are dumped in a file that can be used to create a custom list
                # this is to avoid unuseful long list that can impact performance
                if app_load_method != AppLoadMethod.NotLoad:
                    if app_id in STD_APP_LIST or app_load_method == AppLoadMethod.All:
                        filtered_app_list[app_name] = app_id
                        filtered_app_list_st[app_name] = st_app_id or app_id

                dump_app_list[app_name] = (app_id + ST_APP_SEPARATOR +
                                           st_app_id if st_app_id else app_id)

            except Exception:
                pass

        if self._app_list is None:
            self._app_list = filtered_app_list
            self._app_list_ST = filtered_app_list_st

        if self._dump_apps:
            _LOGGER.info(
                "List of available apps for SamsungTV %s: %s",
                self._attr_name,
                dump_app_list,
            )
            self._dump_apps = False

    def _get_source(self):
        """Return the current input source."""
        if self.state != STATE_ON:
            self._source = None
            return self._source

        if self._running_app != DEFAULT_APP or not self._st:
            self._source = self._running_app
            return self._source

        if self._st.state != STStatus.STATE_ON:
            self._source = self._running_app
            return self._source

        if self._st.source in ["digitalTv", "TV"]:
            cloud_key = "ST_TV"
        else:
            cloud_key = "ST_" + self._st.source

        found_source = self._running_app
        for attr, value in self._source_list.items():
            if value == cloud_key:
                found_source = attr
                break

        self._source = found_source
        return self._source

    async def _smartthings_keys(self, source_key):
        """Manage the SmartThings key commands."""
        if not self._st:
            _LOGGER.error("SmartThings not configured. Command not valid: %s",
                          source_key)
            return False
        if self._st.state != STStatus.STATE_ON:
            _LOGGER.warning("SmartThings not available. Command not sent: %s",
                            source_key)
            return False

        if source_key.startswith("ST_HDMI"):
            await self._st.async_select_source(source_key.replace("ST_", ""))
        elif source_key == "ST_TV":
            await self._st.async_select_source("digitalTv")
        elif source_key == "ST_CHUP":
            await self._st.async_send_command("stepchannel", "up")
        elif source_key == "ST_CHDOWN":
            await self._st.async_send_command("stepchannel", "down")
        elif source_key.startswith("ST_CH"):
            ch_num = source_key.replace("ST_CH", "")
            if ch_num.isdigit():
                await self._st.async_send_command("selectchannel", ch_num)
        elif source_key == "ST_MUTE":
            await self._st.async_send_command("audiomute",
                                              "off" if self._muted else "on")
        elif source_key == "ST_VOLUP":
            await self._st.async_send_command("stepvolume", "up")
        elif source_key == "ST_VOLDOWN":
            await self._st.async_send_command("stepvolume", "down")
        elif source_key.startswith("ST_VOL"):
            vol_lev = source_key.replace("ST_VOL", "")
            if vol_lev.isdigit():
                await self._st.async_send_command("setvolume", vol_lev)
        else:
            _LOGGER.error("Unsupported SmartThings command: %s", source_key)
            return False
        return True

    def _log_st_error(self, st_error):
        """Log start or end problem in ST communication"""
        if self._st_error_count == 0 and not st_error:
            return

        if st_error:
            if self._st_error_count == MAX_ST_ERROR_COUNT:
                return

            self._st_error_count += 1
            if self._st_error_count == MAX_ST_ERROR_COUNT:
                _LOGGER.error(
                    "%s - Error refreshing from SmartThings."
                    " Check connection status with TV on the phone App",
                    self.entity_id,
                )
            return

        if self._st_error_count >= MAX_ST_ERROR_COUNT:
            _LOGGER.warning("%s - Connection to SmartThings restored",
                            self.entity_id)
        self._st_error_count = 0

    async def async_update(self):
        """Update state of device."""

        if self._update_forced():
            return
        """Required to get source and media title"""
        st_error = False
        if self._st:
            use_channel_info = self._get_option(CONF_USE_ST_CHANNEL_INFO, True)
            try:
                async with async_timeout.timeout(ST_UPDATE_TIMEOUT):
                    await self._st.async_device_update(use_channel_info)
            except (
                    asyncio.TimeoutError,
                    ClientConnectionError,
                    ClientResponseError,
            ) as ex:
                st_error = True
                _LOGGER.debug("%s - SmartThings error: [%s]", self.entity_id,
                              ex)

        result = await self.hass.async_add_executor_job(self._ping_device)

        use_mute_check = self._get_option(CONF_USE_MUTE_CHECK, True)
        if not result:
            self._fake_on = None
        elif self._state == STATE_OFF and use_mute_check:
            first_detect = self._fake_on is None
            if first_detect or self._fake_on is True:
                is_muted = await self._upnp.async_get_mute()
                self._fake_on = is_muted or not self._upnp.connected
                if self._fake_on:
                    if first_detect:
                        _LOGGER.debug(
                            "%s - Detected fake power on, status not updated",
                            self.entity_id)
                    result = False

        result = self._delay_power_on(result)
        # result = result and self._ws.is_connected

        if result and self._st:
            if self._st.state != self._st.state.STATE_ON:
                st_error = True
        self._log_st_error(st_error)

        self._state = STATE_ON if result else STATE_OFF

        if self.state == STATE_ON:  # NB: We are checking properties, not attribute!
            if self._delayed_set_source:
                difference = (datetime.now() -
                              self._delayed_set_source_time).total_seconds()
                if difference > DELAYED_SOURCE_TIMEOUT:
                    self._delayed_set_source = None
                else:
                    await self.async_select_source(self._delayed_set_source,
                                                   False)
            await self._update_volume_info()
            await self._get_running_app()
            await self._update_media()

        if self._state == STATE_OFF:
            self._end_of_power_off = None

    def send_command(self,
                     payload,
                     command_type=CMD_SEND_KEY,
                     key_press_delay: float = 0,
                     press=False):
        """Send a key to the tv and handles exceptions."""
        if key_press_delay < 0:
            key_press_delay = None  # means "default" provided with constructor

        ret_val = False
        try:
            if command_type == CMD_RUN_APP:
                ret_val = self._ws.run_app(payload)
            elif command_type == CMD_RUN_APP_REMOTE:
                app_cmd = payload.split(",")
                app_id = app_cmd[0]
                action_type = ""
                if len(app_cmd) > 1:
                    action_type = app_cmd[1]
                ret_val = self._ws.run_app(app_id,
                                           action_type,
                                           "",
                                           use_remote=True)
            elif command_type == CMD_RUN_APP_REST:
                result = self._ws.rest_app_run(payload)
                _LOGGER.debug("Rest API result launching app %s: %s", payload,
                              result)
                ret_val = True
            elif command_type == CMD_OPEN_BROWSER:
                ret_val = self._ws.open_browser(payload)
            elif command_type == CMD_SEND_TEXT:
                ret_val = self._ws.send_text(payload)
            elif command_type == CMD_SEND_KEY:
                hold_delay = 0
                source_keys = payload.split(",")
                key_code = source_keys[0]
                if len(source_keys) > 1:

                    def get_hold_time():
                        hold_time = source_keys[1].replace(" ", "")
                        if not hold_time:
                            return 0
                        if not hold_time.isdigit():
                            return 0
                        hold_time = int(hold_time) / 1000
                        return min(hold_time, KEYHOLD_MAX_DELAY)

                    hold_delay = get_hold_time()

                if hold_delay > 0:
                    ret_val = self._ws.hold_key(key_code, hold_delay)
                else:
                    ret_val = self._ws.send_key(key_code, key_press_delay,
                                                "Press" if press else "Click")
            else:
                _LOGGER.debug("Send command: invalid command type -> %s",
                              command_type)

        except (ConnectionResetError, AttributeError, BrokenPipeError):
            _LOGGER.debug(
                "Error in send_command() -> ConnectionResetError/AttributeError/BrokenPipeError"
            )

        except WebSocketTimeoutException:
            _LOGGER.debug(
                "Failed sending payload %s command_type %s",
                payload,
                command_type,
                exc_info=True,
            )

        except OSError:
            _LOGGER.debug("Error in send_command() -> OSError")

        return ret_val

    async def async_send_command(self,
                                 payload,
                                 command_type=CMD_SEND_KEY,
                                 key_press_delay: float = 0,
                                 press=False):
        """Send a key to the tv in async mode."""
        return await self.hass.async_add_executor_job(self.send_command,
                                                      payload, command_type,
                                                      key_press_delay, press)

    async def _update_media(self):
        """Update media and logo status."""
        logo_option_changed = False
        new_media_title = self._get_new_media_title()

        if not new_media_title:
            self._attr_media_image_url = None
            self._attr_media_title = None
            return

        _LOGGER.debug(
            "New media title is: %s, old media title is: %s, running app is: %s",
            new_media_title,
            self._attr_media_title,
            self._running_app,
        )

        new_logo_option = self._get_option(CONF_LOGO_OPTION, self._logo_option)
        if self._logo_option != new_logo_option:
            self._logo_option = new_logo_option
            self._logo.set_logo_color(new_logo_option)
            logo_option_changed = True

        if new_media_title == self._attr_media_title and not logo_option_changed:
            return

        media_image_url = await self._logo.async_find_match(new_media_title)
        self._attr_media_image_url = media_image_url
        self._attr_media_title = new_media_title

    def _get_new_media_title(self):
        """Get the current media title."""
        if self._state != STATE_ON:
            return None

        if self._st:
            if self._st.state == STStatus.STATE_OFF:
                return None

            if self._running_app == DEFAULT_APP:
                if self._st.source in ["digitalTv", "TV"]:
                    if self._st.channel_name != "":
                        show_channel_number = self._get_option(
                            CONF_SHOW_CHANNEL_NR, False)
                        if show_channel_number and self._st.channel != "":
                            return self._st.channel_name + " (" + self._st.channel + ")"
                        return self._st.channel_name
                    if self._st.channel != "":
                        return self._st.channel

                elif self._st.channel_name != "":
                    # the channel name holds the running app ID
                    # regardless of the self._cloud_source value
                    return self._st.channel_name

        return self._get_source()

    @property
    def media_channel(self):
        """Channel currently playing."""
        if self._state == STATE_ON:
            if self._st:
                if self._st.source in ["digitalTv", "TV"
                                       ] and self._st.channel != "":
                    return self._st.channel
        return None

    @property
    def media_content_type(self):
        """Return the content type of current playing media."""
        if self._state == STATE_ON:
            if self._running_app == DEFAULT_APP:
                if self.media_channel:
                    return MEDIA_TYPE_CHANNEL
                else:
                    return MEDIA_TYPE_VIDEO
            else:
                return MEDIA_TYPE_APP
        return STATE_OFF

    @property
    def app_id(self):
        """ID of the current running app."""
        if self._state == STATE_ON:
            app = None
            if self._app_list_ST and self._running_app != DEFAULT_APP:
                app = self._app_list_ST.get(self._running_app, None)
            if app:
                return app
            elif self._st:
                if not self._st.channel and self._st.channel_name:
                    return self._st.channel_name
            return DEFAULT_APP
        return None

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

        # Warning: we assume that after a sending a power off command, the command is successful
        # so for 20 seconds (defined in POWER_OFF_DELAY) the state will be off regardless of the actual state.
        # This is to have better feedback to the command in the UI, but the logic might cause other issues in the future
        if self._power_off_in_progress():
            return STATE_OFF

        return self._state

    @property
    def source_list(self):
        """List of available input sources."""
        # try to get source list from SmartThings if a custom source list is not defined
        if self._st and self._default_source_used:
            self._get_st_sources()

        self._gen_installed_app_list()

        source_list = []
        source_list.extend(list(self._source_list))
        if self._app_list:
            source_list.extend(list(self._app_list))
        if self._channel_list:
            source_list.extend(list(self._channel_list))
        return source_list

    @property
    def channel_list(self):
        """List of available channels."""
        if self._channel_list is None:
            return None
        return list(self._channel_list)

    @property
    def source(self):
        """Return the current input source."""
        return self._get_source()

    @property
    def sound_mode(self):
        """Name of the current sound mode."""
        if self._st:
            return self._st.sound_mode
        return None

    @property
    def sound_mode_list(self):
        """List of available sound modes."""
        if self._st:
            return self._st.sound_mode_list or None
        return None

    def _send_wol_packet(self, wol_repeat=None):
        """Send a WOL packet to turn on the TV."""
        if not self._mac:
            _LOGGER.error(
                "MAC address not configured, impossible send WOL packet")
            return False

        if not wol_repeat:
            wol_repeat = self._get_option(CONF_WOL_REPEAT, 1)
        wol_repeat = max(1, min(wol_repeat, MAX_WOL_REPEAT))
        ip_address = self._broadcast or "255.255.255.255"
        send_success = False
        for i in range(wol_repeat):
            if i > 0:
                sleep(0.25)
            try:
                send_magic_packet(self._mac, ip_address=ip_address)
                send_success = True
            except socketError as exc:
                _LOGGER.warning(
                    "Failed tentative n.%s to send WOL packet: %s",
                    i,
                    exc,
                )
            except (TypeError, ValueError) as exc:
                _LOGGER.error("Error sending WOL packet: %s", exc)
                return False

        return send_success

    async def _async_power_on(self, set_art_mode=False):
        """Turn the media player on."""
        cmd_power_on = "KEY_POWER"
        cmd_power_art = "KEY_POWER"
        if set_art_mode:
            if self._ws.artmode_status == ArtModeStatus.Off:
                # art mode from on
                await self.async_send_command(cmd_power_art)
                self._state = STATE_OFF
                return True

        if self._ws.artmode_status == ArtModeStatus.On:
            if set_art_mode:
                return False
            # power on from art mode
            await self.async_send_command(cmd_power_art)
            return True

        if self.state != STATE_OFF:
            return False

        result = True
        if not await self.async_send_command(cmd_power_on):
            turn_on_method = PowerOnMethod(
                self._get_option(CONF_POWER_ON_METHOD,
                                 PowerOnMethod.WOL.value))

            if turn_on_method == PowerOnMethod.SmartThings and self._st:
                await self._st.async_turn_on()
            else:
                result = await self.hass.async_add_executor_job(
                    self._send_wol_packet)

        if result:
            self._state = STATE_OFF
            self._end_of_power_off = None
            self._ws.set_power_on_request(set_art_mode)

        return result

    async def _async_turn_on(self, set_art_mode=False):
        """Turn the media player on."""
        self._delayed_set_source = None
        if not await self._async_power_on(set_art_mode):
            return False
        if self._state == STATE_OFF:

            def update_status():
                if self._state != STATE_ON:
                    self.async_schedule_update_ha_state(True)
                    self._set_update_forced = True

            self.hass.loop.call_later(POWER_ON_DELAY, update_status)
            self._power_on_detected = datetime.min
            await self._async_switch_entity(not set_art_mode)

        return True

    async def async_turn_on(self):
        """Turn the media player on."""
        await self._async_turn_on()

    async def async_set_art_mode(self):
        """Turn the media player on setting in art mode."""
        await self._async_turn_on(True)

    def _turn_off(self):
        """Turn off media player."""
        if self._power_off_in_progress():
            return False

        cmd_power_off = "KEY_POWER"
        cmd_power_art = "KEY_POWER"
        self._ws.set_power_off_request()
        if self._state == STATE_ON:
            if self._ws.artmode_status == ArtModeStatus.Unsupported:
                self.send_command(cmd_power_off)
            else:
                self.send_command(f"{cmd_power_art},3000")
        elif self._ws.artmode_status == ArtModeStatus.On:
            self.send_command(f"{cmd_power_art},3000")
        else:
            return False

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

        return True

    async def async_turn_off(self):
        """Turn the media player on."""
        result = await self.hass.async_add_executor_job(self._turn_off)
        if result:
            await self._async_switch_entity(False)

    @property
    def volume_level(self):
        """Volume level of the media player (0..1)."""
        # self._volume = int(self._upnp.get_volume()) / 100
        if self.support_volume_set:
            return self._volume
        else:
            return None

    @property
    def is_volume_muted(self):
        """Boolean if volume is currently muted."""
        # self._muted = self._upnp.get_mute()
        return self._muted

    def volume_up(self):
        """Volume up the media player."""
        self.send_command("KEY_VOLUP")
        if self.support_volume_set:
            self._volume = min(1.0, self._volume + 0.01)

    def volume_down(self):
        """Volume down media player."""
        self.send_command("KEY_VOLDOWN")
        if self.support_volume_set:
            self._volume = max(0.0, self._volume - 0.01)

    def mute_volume(self, mute):
        """Send mute command."""
        self.send_command("KEY_MUTE")
        if self.support_volume_set:
            self._muted = False if self._muted else True

    async def async_set_volume_level(self, volume):
        """Set the volume level."""
        if self._st and self._setvolumebyst:
            await self._st.async_send_command("setvolume", int(volume * 100))
        else:
            await self._upnp.async_set_volume(int(volume * 100))
        self._volume = volume

    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_command("KEY_PLAY")

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

    def media_stop(self):
        """Send media pause command to media player."""
        self._playing = False
        self.send_command("KEY_STOP")

    def media_next_track(self):
        """Send next track command."""
        if self.media_channel:
            self.send_command("KEY_CHUP")
        else:
            self.send_command("KEY_FF")

    def media_previous_track(self):
        """Send the previous track command."""
        if self.media_channel:
            self.send_command("KEY_CHDOWN")
        else:
            self.send_command("KEY_REWIND")

    async def _async_send_keys(self, source_key):
        """Send key / chained keys."""
        prev_wait = True

        if "+" in source_key:
            all_source_keys = source_key.split("+")
            for this_key in all_source_keys:
                if this_key.isdigit():
                    prev_wait = True
                    await asyncio.sleep(
                        min(
                            max((int(this_key) / 1000), KEYPRESS_MIN_DELAY),
                            KEYPRESS_MAX_DELAY,
                        ))
                else:
                    # put a default delay between key if set explicit
                    if not prev_wait:
                        await asyncio.sleep(KEYPRESS_DEFAULT_DELAY)
                    prev_wait = False
                    if this_key.startswith("ST_"):
                        await self._smartthings_keys(this_key)
                    else:
                        await self.async_send_command(this_key)

            return True

        if source_key.startswith("ST_"):
            return await self._smartthings_keys(source_key)

        return await self.async_send_command(source_key)

    async def _async_set_channel_source(self, channel_source=None):
        """Select the source for a channel."""

        if not channel_source:
            if self._running_app == DEFAULT_APP:
                return True
            _LOGGER.error("Current source invalid for channel")
            return False

        if self._source == channel_source:
            return True

        if channel_source not in self._source_list:
            _LOGGER.error("Invalid channel source: %s", channel_source)
            return False

        await self.async_select_source(channel_source)
        if self._source != channel_source:
            _LOGGER.error("Error selecting channel source: %s", channel_source)
            return False
        await asyncio.sleep(3)

        return True

    async def _async_set_channel(self, channel):
        """Set a specific channel."""

        if channel.startswith("http"):
            await self.async_play_media(MEDIA_TYPE_URL, channel)
            return True

        channel_cmd = channel.split("@")
        channel_no = channel_cmd[0]
        channel_source = None
        if len(channel_cmd) > 1:
            channel_source = channel_cmd[1]

        try:
            cv.positive_int(channel_no)
        except vol.Invalid:
            _LOGGER.error("Channel must be positive integer")
            return False

        if not await self._async_set_channel_source(channel_source):
            return False

        if self._st:
            return await self._smartthings_keys(f"ST_CH{channel_no}")

        def send_digit():
            for digit in channel_no:
                self.send_command("KEY_" + digit)
                sleep(KEYPRESS_DEFAULT_DELAY)
            self.send_command("KEY_ENTER")

        await self.hass.async_add_executor_job(send_digit)
        return True

    async def _async_launch_app(self, app_data):
        """Launch app with different methods."""

        method = CMD_RUN_APP
        app_cmd = app_data.split("@")
        app_id = app_cmd[0]
        if len(app_cmd) > 1:
            method = app_cmd[1]
        else:
            app_launch_method = AppLaunchMethod(
                self._get_option(CONF_APP_LAUNCH_METHOD,
                                 AppLaunchMethod.Standard.value))

            if app_launch_method == AppLaunchMethod.Remote:
                method = CMD_RUN_APP_REMOTE
            elif app_launch_method == AppLaunchMethod.Rest:
                method = CMD_RUN_APP_REST

        await self.async_send_command(app_id, method)

    async def async_play_media(self, media_type, media_id, **kwargs):
        """Support running different media type command."""
        media_type = media_type.lower()

        # Type channel
        if media_type == MEDIA_TYPE_CHANNEL:
            await self._async_set_channel(media_id)

        # Launch an app
        elif media_type == MEDIA_TYPE_APP:
            await self._async_launch_app(media_id)

        # Send custom key
        elif media_type == MEDIA_TYPE_KEY:
            try:
                cv.string(media_id)
            except vol.Invalid:
                _LOGGER.error('Media ID must be a string (ex: "KEY_HOME"')
                return

            source_key = media_id
            await self._async_send_keys(source_key)

        # Play media
        elif media_type == MEDIA_TYPE_URL:
            try:
                cv.url(media_id)
            except vol.Invalid:
                _LOGGER.error('Media ID must be an url (ex: "http://"')
                return

            await self._upnp.async_set_current_media(media_id)
            self._playing = True

        # Trying to make stream component work on TV
        elif media_type == "application/vnd.apple.mpegurl":
            await self._upnp.async_set_current_media(media_id)
            self._playing = True

        elif media_type == MEDIA_TYPE_BROWSER:
            await self.async_send_command(media_id, CMD_OPEN_BROWSER)

        elif media_type == MEDIA_TYPE_TEXT:
            await self.async_send_command(media_id, CMD_SEND_TEXT)

        else:
            _LOGGER.error("Unsupported media type: %s", media_type)
            return

    async def async_select_source(self, source, reset_delayed=True):
        """Select input source."""
        if not reset_delayed:
            if self._st:
                if self._st.state != STStatus.STATE_ON:
                    # wait for smartthings available
                    return

        running_app = DEFAULT_APP
        self._delayed_set_source = None

        if self.state != STATE_ON:
            if await self._async_turn_on():
                self._delayed_set_source = source
                self._delayed_set_source_time = datetime.now()
            return

        if self._source_list and source in self._source_list:
            source_key = self._source_list[source]
            if not await self._async_send_keys(source_key):
                return
        elif self._app_list and source in self._app_list:
            app_id = self._app_list[source]
            running_app = source
            await self._async_launch_app(app_id)
            if self._st:
                self._st.set_application(self._app_list_ST[source])
        elif self._channel_list and source in self._channel_list:
            source_key = self._channel_list[source]
            await self._async_set_channel(source_key)
            return
        else:
            _LOGGER.error("Unsupported source")
            return

        self._running_app = running_app
        self._source = source

    async def async_select_sound_mode(self, sound_mode):
        """Select sound mode."""
        if not self._st:
            raise NotImplementedError()
        await self._st.async_set_sound_mode(sound_mode)

    async def async_select_picture_mode(self, picture_mode):
        """Select picture mode."""
        if not self._st:
            raise NotImplementedError()
        await self._st.async_set_picture_mode(picture_mode)

    @property
    def extra_state_attributes(self):
        """Return the optional state attributes."""
        data = {ATTR_IP_ADDRESS: self._host}
        if self._ws.artmode_status != ArtModeStatus.Unsupported:
            status_on = self._ws.artmode_status == ArtModeStatus.On
            data.update(
                {ATTR_ART_MODE_STATUS: STATE_ON if status_on else STATE_OFF})
        if self._st:
            picture_mode = self._st.picture_mode
            picture_mode_list = self._st.picture_mode_list
            if picture_mode:
                data[ATTR_PICTURE_MODE] = picture_mode
            if picture_mode_list:
                data[ATTR_PICTURE_MODE_LIST] = picture_mode_list

        return data

    async def async_will_remove_from_hass(self):
        """Run when entity will be removed from hass."""
        await self.hass.async_add_executor_job(self._ws.stop_client)

    async def _async_switch_entity(self, power_on: bool):
        """Switch on/off related configure HA entity."""

        if power_on:
            service_name = f"{HA_DOMAIN}.{SERVICE_TURN_ON}"
            conf_entity = CONF_SYNC_TURN_ON
        else:
            service_name = f"{HA_DOMAIN}.{SERVICE_TURN_OFF}"
            conf_entity = CONF_SYNC_TURN_OFF

        entity_list = self._get_option(conf_entity)
        if not entity_list:
            return

        for index, entity in enumerate(entity_list):
            if index >= MAX_CONTROLLED_ENTITY:
                _LOGGER.warning(
                    "SamsungTV Smart - Maximum %s entities can be controlled",
                    MAX_CONTROLLED_ENTITY,
                )
                break
            if entity:
                await _async_call_service(self.hass, service_name, entity)

        return