"""Support for interface with an Samsung TV."""
import asyncio
import json
import logging
import os
from datetime import datetime, timedelta
from socket import error as socketError
from time import sleep
from wakeonlan import send_magic_packet
from websocket import WebSocketTimeoutException

import voluptuous as vol

from aiohttp import ClientConnectionError, ClientSession, ClientResponseError
from async_timeout import timeout

from .api.samsungws import SamsungTVWS, ArtModeStatus
from .api.smartthings import SmartThingsTV, STStatus
from .api.upnp import upnp

from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.service import async_call_from_config, CONF_SERVICE_ENTITY_ID
from homeassistant.util import dt as dt_util, Throttle
from homeassistant.components.media_player import DEVICE_CLASS_TV

from homeassistant.components.media_player.const import (
    MEDIA_TYPE_APP,
    MEDIA_TYPE_CHANNEL,
    MEDIA_TYPE_URL,
    MEDIA_TYPE_VIDEO,
    SUPPORT_NEXT_TRACK,
    SUPPORT_PAUSE,
    SUPPORT_PLAY,
    SUPPORT_PLAY_MEDIA,
    SUPPORT_PREVIOUS_TRACK,
    SUPPORT_SELECT_SOUND_MODE,
    SUPPORT_SELECT_SOURCE,
    SUPPORT_STOP,
    SUPPORT_TURN_OFF,
    SUPPORT_TURN_ON,
    SUPPORT_VOLUME_MUTE,
    SUPPORT_VOLUME_SET,
    SUPPORT_VOLUME_STEP,
)

from homeassistant.const import (
    CONF_API_KEY,
    CONF_BROADCAST_ADDRESS,
    CONF_DEVICE_ID,
    CONF_HOST,
    CONF_ID,
    CONF_MAC,
    CONF_NAME,
    CONF_PORT,
    CONF_SERVICE,
    CONF_SERVICE_DATA,
    CONF_TIMEOUT,
    STATE_OFF,
    STATE_ON,
    STATE_UNAVAILABLE,
)

from .const import (
    DOMAIN,
    CONF_APP_LAUNCH_METHOD,
    CONF_APP_LIST,
    CONF_APP_LOAD_METHOD,
    CONF_CHANNEL_LIST,
    CONF_DEVICE_MODEL,
    CONF_DEVICE_NAME,
    CONF_DEVICE_OS,
    CONF_POWER_ON_DELAY,
    CONF_POWER_ON_METHOD,
    CONF_SHOW_CHANNEL_NR,
    CONF_SOURCE_LIST,
    CONF_SYNC_TURN_OFF,
    CONF_SYNC_TURN_ON,
    CONF_USE_MUTE_CHECK,
    CONF_USE_ST_CHANNEL_INFO,
    CONF_USE_ST_STATUS_INFO,
    CONF_WOL_REPEAT,
    CONF_WS_NAME,
    DEFAULT_APP,
    DEFAULT_POWER_ON_DELAY,
    DEFAULT_SOURCE_LIST,
    DEFAULT_TIMEOUT,
    MAX_WOL_REPEAT,
    SERVICE_SELECT_PICTURE_MODE,
    SERVICE_SET_ART_MODE,
    STD_APP_LIST,
    WS_PREFIX,
    AppLoadMethod,
    AppLaunchMethod,
    PowerOnMethod,
)

try:
    from homeassistant.components.media_player import MediaPlayerEntity
except ImportError:
    from homeassistant.components.media_player import MediaPlayerDevice as MediaPlayerEntity

ATTR_ART_MODE_STATUS = "art_mode_status"
ATTR_DEVICE_MODEL = "device_model"
ATTR_DEVICE_NAME = "device_name"
ATTR_IP_ADDRESS = "ip_address"
ATTR_PICTURE_MODE = "picture_mode"
ATTR_PICTURE_MODE_LIST = "picture_mode_list"

CMD_OPEN_BROWSER = "open_browser"
CMD_RUN_APP = "run_app"
CMD_RUN_APP_REMOTE = "run_app_remote"
CMD_RUN_APP_REST = "run_app_rest"
CMD_SEND_KEY = "send_key"
CMD_SEND_TEXT = "send_text"

DELAYED_SOURCE_TIMEOUT = 80
KEYHOLD_MAX_DELAY = 5.0
KEYPRESS_DEFAULT_DELAY = 0.5
KEYPRESS_MAX_DELAY = 2.0
KEYPRESS_MIN_DELAY = 0.2
MAX_ST_ERROR_COUNT = 4
MAX_ST_CONN_ERROR_COUNT = 3
MEDIA_TYPE_BROWSER = "browser"
MEDIA_TYPE_KEY = "send_key"
MEDIA_TYPE_TEXT = "send_text"
POWER_OFF_DELAY = 20
POWER_ON_DELAY = 5
ST_APP_SEPARATOR = "/"
ST_UPDATE_TIMEOUT = 5

SERVICE_TURN_OFF = "homeassistant.turn_off"
SERVICE_TURN_ON = "homeassistant.turn_on"
MAX_CONTROLLED_ENTITY = 4

MIN_TIME_BETWEEN_APP_SCANS = timedelta(seconds=60)

SUPPORT_SAMSUNGTV_SMART = (
    SUPPORT_PAUSE
    | SUPPORT_PLAY
    | SUPPORT_PLAY_MEDIA
    | SUPPORT_STOP
    | SUPPORT_VOLUME_STEP
    | SUPPORT_VOLUME_MUTE
    | SUPPORT_VOLUME_SET
    | SUPPORT_PREVIOUS_TRACK
    | SUPPORT_NEXT_TRACK
    | SUPPORT_SELECT_SOURCE
    | SUPPORT_TURN_ON
    | SUPPORT_TURN_OFF
)

SCAN_INTERVAL = timedelta(seconds=15)

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass, config_entry, async_add_entities):
    """Set up the Samsung TV from a config entry."""

    # session used by aiohttp
    session = hass.helpers.aiohttp_client.async_get_clientsession()

    entry_id = config_entry.entry_id
    config = config_entry.data.copy()
    add_conf = hass.data[DOMAIN][config_entry.unique_id]
    for attr, value in add_conf.items():
        if value:
            config[attr] = value
    _LOGGER.debug(config)

    async_add_entities([SamsungTVDevice(config, entry_id, session)], True)

    # register services
    platform = entity_platform.current_platform.get()
    platform.async_register_entity_service(
        SERVICE_SELECT_PICTURE_MODE,
        {vol.Required(ATTR_PICTURE_MODE): cv.string},
        "async_select_picture_mode",
    )
    platform.async_register_entity_service(
        SERVICE_SET_ART_MODE,
        None,
        "async_set_art_mode",
    )

    _LOGGER.info(
        "Samsung TV %s:%d added as '%s'",
        config.get(CONF_HOST),
        config.get(CONF_PORT),
        config.get(CONF_NAME),
    )


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

    def __init__(self, config, entry_id, session: ClientSession):
        """Initialize the Samsung device."""

        # Save a reference to the imported classes
        self._entry_id = entry_id
        self._session = session
        self._host = config.get(CONF_HOST)
        self._name = config.get(CONF_NAME)
        self._uuid = config.get(CONF_ID)
        self._mac = config.get(CONF_MAC)
        self._device_name = config.get(CONF_DEVICE_NAME)
        self._device_model = config.get(CONF_DEVICE_MODEL)
        self._device_os = config.get(CONF_DEVICE_OS)
        self._broadcast = config.get(CONF_BROADCAST_ADDRESS)
        self._timeout = config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT)

        port = config.get(CONF_PORT)
        api_key = config.get(CONF_API_KEY, None)
        device_id = config.get(CONF_DEVICE_ID, None)

        # 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
        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
        self._st_conn_error_count = 0

        self._token_file = None

        # Generate token file only for WS + SSL + Token connection
        if port == 8002:
            self._gen_token_file()

        ws_name = config.get(CONF_WS_NAME, self._name)
        self._ws = SamsungTVWS(
            name=WS_PREFIX
            + " "
            + ws_name,  # this is the name shown in the TV list of external device.
            host=self._host,
            port=port,
            timeout=self._timeout,
            key_press_delay=KEYPRESS_DEFAULT_DELAY,
            token_file=self._token_file,
            app_list=self._app_list,
        )

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

        self._st = None
        if api_key and device_id:
            self._st = SmartThingsTV(
                api_key=api_key,
                device_id=device_id,
                use_channel_info=True,
                session=session,
            )

        self._st_error_count = 0
        self._setvolumebyst = False

    @staticmethod
    def _load_param_list(src_list):

        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):
        retval = {"app": {}, "appST": {}}

        for attr, 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, "")
                st_app_id = st_app_id if st_app_id != "" else app_id
            else:
                st_app_id = value_split[1]
            retval["app"].update({attr: app_id})
            retval["appST"].update({attr: st_app_id})

        return retval

    def _get_option(self, param, default=None):
        options = self.hass.data[DOMAIN][self._entry_id].get("options", {})
        return options.get(param, default)

    def _gen_token_file(self):
        self._token_file = (
            os.path.dirname(os.path.realpath(__file__))
            + "/token-"
            + self._host
            + ".txt"
        )

        if os.path.isfile(self._token_file) is False:
            # For correct auth
            self._timeout = 45

            # Create token file for catch possible errors
            try:
                handle = open(self._token_file, "w+")
                handle.close()
            except:
                _LOGGER.error(
                    "Samsung TV - Error creating token file: %s", self._token_file
                )

    def _delete_token_file(self):

        if not self._token_file:
            return

        if os.path.isfile(self._token_file) is True:

            # delete token file for catch possible errors
            try:
                os.remove(self._token_file)
            except:
                _LOGGER.error(
                    "Samsung TV - Error deleting token file: %s", self._token_file
                )

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

    def _update_forced(self):
        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):
        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):
        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 _ping_device(self):

        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:
            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):

        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):
        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):
        if self._state != STATE_ON:
            _LOGGER.debug("Samsung TV is OFF, _gen_installed_app_list not executed")
            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
        clean_app_list = {}
        clean_app_list_ST = {}
        dump_app_list = {}
        for app in app_list.values():
            try:
                app_name = app.app_name
                app_id = app.app_id
                full_app_id = 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 st_app_id != "###" or app_load_method == AppLoadMethod.All:
                        clean_app_list[app_name] = app_id
                        clean_app_list_ST[app_name] = (
                            st_app_id if st_app_id != "" else app_id
                        )
                        full_app_id = (
                            app_id + ST_APP_SEPARATOR + st_app_id
                            if st_app_id != "" and st_app_id != "###"
                            else app_id
                        )

                dump_app_list[app_name] = full_app_id

            except Exception:
                pass

        self._app_list = clean_app_list
        self._app_list_ST = clean_app_list_ST
        try:
            dump_file_name = (
                os.path.dirname(os.path.realpath(__file__))
                + "/applist-"
                + self._host
                + ".txt"
            )
            with open(dump_file_name, "w") as dump_file:
                dump_file.write('app_list: "' + str(dump_app_list) + '"')
        except OSError:
            _LOGGER.error("Failed to write dump apps file")
            pass

        _LOGGER.debug("Dump of available apps:%s", dump_app_list)

    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):
        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

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

        if self._update_forced():
            return

        """Required to get source and media title"""
        if self._st:
            use_channel_info = self._get_option(CONF_USE_ST_CHANNEL_INFO, True)
            try:
                with timeout(ST_UPDATE_TIMEOUT):
                    await self._st.async_device_update(use_channel_info)
                self._st_error_count = 0
            except (
                asyncio.TimeoutError,
                ClientConnectionError,
                ClientResponseError,
            ) as ex:
                self._st_error_count += 1
                _LOGGER.debug("SamsungTV Smart - Error: [%s]", ex)

        if self._st_error_count >= MAX_ST_ERROR_COUNT:
            _LOGGER.error("SamsungTV Smart - Error refreshing from SmartThings")
            self._st_error_count = 0

        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("SamsungTV Smart - Detected fake power on, status not updated")
                    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:
                if self._st_conn_error_count < MAX_ST_CONN_ERROR_COUNT:
                    self._st_conn_error_count += 1
                    if self._st_conn_error_count == MAX_ST_CONN_ERROR_COUNT:
                        _LOGGER.warning(
                            "SamsungTV Smart - SmartThings connection is offline."
                            " Check connection status on the phone App"
                        )
            else:
                if self._st_conn_error_count >= MAX_ST_CONN_ERROR_COUNT:
                    _LOGGER.warning("SamsungTV Smart - SmartThings connection now is online")
                self._st_conn_error_count = 0

        self._state = STATE_ON if result else STATE_OFF

        if self.state == STATE_ON:
            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()

        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
    ):
        return await self.hass.async_add_executor_job(
            self.send_command, payload, command_type, key_press_delay, press
        )

    @property
    def unique_id(self) -> str:
        """Return the unique ID of the device."""
        return self._uuid

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

    @property
    def icon(self):
        return "mdi:television"

    @property
    def media_title(self):
        """Title of current playing media."""
        if self._state != STATE_ON:
            return None

        if self._st:

            if self._st.state == STStatus.STATE_OFF:
                return None
            elif self._running_app == DEFAULT_APP:
                if self._st.source in ["digitalTv", "TV"]:
                    show_channel_number = self._get_option(CONF_SHOW_CHANNEL_NR, False)
                    if self._st.channel_name != "":
                        if show_channel_number and self._st.channel != "":
                            return self._st.channel_name + " (" + self._st.channel + ")"
                        return self._st.channel_name
                    elif 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()

        if self._app_list is None:
            if self._ws.installed_app:
                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

    @property
    def supported_features(self):
        """Flag media player features that are supported."""
        features = SUPPORT_SAMSUNGTV_SMART
        if self._st:
            features |= SUPPORT_SELECT_SOUND_MODE
        return features

    @property
    def device_class(self):
        """Set the device class to TV."""
        return DEVICE_CLASS_TV

    def _send_wol_packet(self, wol_repeat=None):
        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, 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, 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):
        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 source in self._source_list:
            source_key = self._source_list[source]
            if not await self._async_send_keys(source_key):
                return
        elif 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 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 device_info(self):
        """Return a device description for device registry."""
        _device_info = {
            "identifiers": {(DOMAIN, f"{self._uuid}")},
            "manufacturer": "Samsung Electronics",
            "name": self.name,
            "connections": {(CONNECTION_NETWORK_MAC, self._mac)},
        }
        model = self._device_model if self._device_model else "Samsung TV"
        if self._device_name:
            model = "%s (%s)" % (model, self._device_name)
        _device_info["model"] = model
        if self._device_os:
            _device_info["sw_version"] = self._device_os

        return _device_info

    @property
    def device_state_attributes(self):
        """Return the optional state attributes."""
        data = {
            ATTR_IP_ADDRESS: self._host
        }
        if self._device_model:
            data[ATTR_DEVICE_MODEL] = self._device_model
        if self._device_name:
            data[ATTR_DEVICE_NAME] = self._device_name
        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

    def _will_remove_from_hass(self):
        self._ws.stop_client()
        self._delete_token_file()

    async def async_will_remove_from_hass(self):
        await self.hass.async_add_executor_job(self._will_remove_from_hass)

    async def _async_switch_entity(self, power_on: bool):

        if power_on:
            service_name = SERVICE_TURN_ON
            conf_entity = CONF_SYNC_TURN_ON
        else:
            service_name = SERVICE_TURN_OFF
            conf_entity = CONF_SYNC_TURN_OFF

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

        entity_array = entity_list.split(",")

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

        return

    async def _async_call_service(
            self,
            service_name,
            entity_id,
            variable_data=None,
    ):
        service_data = {
            CONF_SERVICE: service_name,
            CONF_SERVICE_ENTITY_ID: entity_id,
        }

        if variable_data:
            service_data[CONF_SERVICE_DATA] = variable_data

        try:
            await async_call_from_config(
                self.hass, service_data, blocking=False, validate_config=True,
            )
        except HomeAssistantError as ex:
            _LOGGER.error("SamsungTV Smart - error %s", ex)

        return
