kdschlosser / samsungctl

Remote control Samsung televisions via a TCP/IP connection
MIT License
148 stars 34 forks source link

Mute not working #99

Open jgrieger1 opened 5 years ago

jgrieger1 commented 5 years ago

On my TV, the UPNP SetMute method is expecting a Boolean true/false parameter and the GetMute is returning a Boolean true/false value. The set method in samsungctl is trying to pass a string value of 'Enable' or 'Disable' and the getter is expecting 'Enable' or 'Disable' to be returned from the UPNP GetMute method. Below is the UPNP method info dumped from my TV:

    Method name: GetMute
    Access point: RemoteWrapper.RenderingControl.GetMute
    Call: CurrentMute = RemoteWrapper.RenderingControl.GetMute(InstanceID, Channel)
    ----------------------------------------------
        Parameters:

        InstanceID:
            UPNP data type: A_ARG_TYPE_InstanceID
            Py data type: unsigned 32bit int

        Channel:
            UPNP data type: A_ARG_TYPE_Channel
            Py data type: <class 'str'>, <class 'bytes'>
            Allowed values:
                Master

        Return Values:

        CurrentMute:
            UPNP data type: Mute
            Py data type: <class 'bool'>
        Possible returned values: True/False

    Method name: SetMute
    Access point: RemoteWrapper.RenderingControl.SetMute
    Call: RemoteWrapper.RenderingControl.SetMute(InstanceID, Channel, DesiredMute)
    ----------------------------------------------
        Parameters:

        InstanceID:
            UPNP data type: A_ARG_TYPE_InstanceID
            Py data type: unsigned 32bit int

        Channel:
            UPNP data type: A_ARG_TYPE_Channel
            Py data type: <class 'str'>, <class 'bytes'>
            Allowed values:
                Master

        DesiredMute:
            UPNP data type: Mute
            Py data type: <class 'bool'>
            Allowed values: True/False

        Return Values: None
jgrieger1 commented 5 years ago

I also found that RenderingControl.GetMute returns a list data type with the True/False value in the first position of the list.

kdschlosser commented 5 years ago

do this instead of accessing the UPNP function directly.

# config assumed

with samsungctl.Remote(config) as remote:
    print(remote.mute)
    remote.mute = True

the reason I say to use that is because there are different ways to set the mute. some TV's have GetMute and SetMute and others have GetChannelMute SetChannelMute

when dealing with UPNP and the boolean data types the value that needs to bee passed or the value that gets returned can vary,

these are the choices a boolean data type can take. it is only going to take one set. it's a True False thing

Disable, Enable disable, enable Off, On off, on 0, 1 false, true False, True no, yes No, Yes

so instead of having the possibility of varying choices. I opted to make it a boolean data type.. a simple True False.

now the way the code is done you can pass a string with one of the values the device has set up for that function.

I believe on the Samsung TV's it is going to be one of the values below..

disable, enable Disable, Enable

you can check simply by doing


# confiig assumed

with samsingctl.Remote(config) as remote:
    for param in remote.RenderingControl.GetMute.params:
        print(param.__name__, param.allowed_values)

I also found that RenderingControl.GetMute returns a list data type with the True/False value in the first position of the list.

I am not sure what you mean by that.. the only thing that returns a False, True list is the as_dict property..

here is the code for the boolean data type.


class Boolean(object):
    py_data_type = (bool,)

    def __init__(self, name, data_type_name, node, direction):
        self.__name__ = name
        self.data_type_name = data_type_name
        self.direction = direction
        allowed = node.find('allowedValueList')

        if allowed is None:
            allowed_values = ['0', '1']
        else:
            allowed_values = list(value.text for value in allowed)
            if 'yes' in allowed_values:
                allowed_values = ['no', 'yes']
            elif 'Yes' in allowed_values:
                allowed_values = ['No', 'Yes']
            elif 'enable' in allowed_values:
                allowed_values = ['disable', 'enable']
            elif 'Enable' in allowed_values:
                allowed_values = ['Disable', 'Enable']
            elif 'on' in allowed_values:
                allowed_values = ['off', 'on']
            elif 'On' in allowed_values:
                allowed_values = ['Off', 'On']
            elif 'true' in allowed_values:
                allowed_values = ['false', 'true']
            elif 'True' in allowed_values:
                allowed_values = ['False', 'True']
            else:
                allowed_values = ['0', '1']

        self.allowed_values = allowed_values

        default_value = node.find('defaultValue')
        if default_value is not None:
            if default_value.text == 'NOT_IMPLEMENTED':
                self.default_value = 'NOT_IMPLEMENTED'
            else:
                default_value = default_value.text
                if default_value in (
                    'yes',
                    'Yes',
                    'true',
                    'True',
                    '1',
                    'enable',
                    'Enable'
                ):
                    default_value = True
                else:
                    default_value = False

        self.default_value = default_value

    def __str__(self, indent=''):
        output = TEMPLATE.format(
            indent=indent,
            name=self.__name__,
            upnp_data_type=self.data_type_name,
            py_data_type=bool
        )

        if self.default_value == 'NOT_IMPLEMENTED':
            return output + indent + '    NOT_IMPLEMENTED' + '\n'

        if self.default_value is not None:
            output += (
                indent +
                '    Default: ' +
                repr(self.default_value) +
                '\n'
            )

        if self.direction == 'in':
            output += indent + '    Allowed values: True/False\n'
        else:
            output += indent + 'Possible returned values: True/False\n'

        return output

    @property
    def as_dict(self):
        res = dict(
            name=self.__name__,
            default_value=self.default_value,
            data_type=self.py_data_type
        )
        if self.direction == 'in':
            res['allowed_values'] = [False, True]
        else:
            res['returned_values'] = [False, True]

        return res

    def __call__(self, value):
        if value is None:
            if self.default_value is None:
                if self.direction == 'in':
                    raise ValueError('A value must be supplied')

            else:
                value = self.default_value
                if self.direction == 'out':
                    value = self.allowed_values[int(value)]

        if self.direction == 'in':

            if isinstance(value, bool):
                value = self.allowed_values[int(value)]

            if value not in self.allowed_values:
                raise TypeError('Incorrect value')

        elif value is not None:
            if self.default_value == 'NOT_IMPLEMENTED':
                value = self.default_value
            else:
                value = bool(self.allowed_values.index(value))

        return value
jgrieger1 commented 5 years ago

Sorry, I didn't explain the issue very well. It is not with the UPNP methods, but with the samsungctl mute property. I was trying to use the samsungctl mute property, but it was not working so I dug deeper into using the UPNP methods directly. Using the UPNP methods RenderingControl.GetMute and RenderingControl.SetMute I'm able to retrieve the mute state and set the mute on the TV. The RenderingControl.GetMute returns a True/False value stored in the first position of a list data type. I'm able to set mute on and off using the RenderingControl.SetMute UPNP method by passing either True or False as a parameter. The UPNP methods MainTVAgent2.GetMuteStatus and MainTVAgent2.SetMute do not work on my TV and trying to use these returns an AttributeError.

the samsungctl mute property always returns true. In the below, mute is currently disabled on the TV but remote.mute returns True.

>>> print(remote.mute)
True

Trying to set the mute using remote.mute I get an error:

>>> remote.mute = False
Traceback (most recent call last):
  File "/home/jeff/samsungctl/samsungctl/samsungctl/upnp/__init__.py", line 597, in mute
    self.MainTVAgent2.SetMute(desired_mute)
  File "/home/jeff/samsungctl/samsungctl/samsungctl/upnp/UPNP_Device/upnp_class.py", line 127, in __getattr__
    raise AttributeError(item)
AttributeError: MainTVAgent2

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/jeff/samsungctl/samsungctl/samsungctl/upnp/__init__.py", line 599, in mute
    self.set_channel_mute('Master', desired_mute)
  File "/home/jeff/samsungctl/samsungctl/samsungctl/upnp/__init__.py", line 111, in set_channel_mute
    self.RenderingControl.SetMute(0, channel, desired_mute)
  File "/home/jeff/samsungctl/samsungctl/samsungctl/upnp/UPNP_Device/action.py", line 67, in __call__
    value = param(kwargs[param.__name__])
  File "/home/jeff/samsungctl/samsungctl/samsungctl/upnp/UPNP_Device/data_type.py", line 385, in __call__
    raise TypeError('Incorrect value')
TypeError: Incorrect value

Using the UPNP method RenderingControl.GetMute I can successfully retrieve the current mute state:

>>> remote.RenderingControl.GetMute(0,'Master')
[False]

And using the UPNP method RenderingControl.SetMute I can successfully set the current mute state:

>>> remote.RenderingControl.SetMute(0,'Master',True)
[]
>>> remote.RenderingControl.GetMute(0,'Master')
[True]

The data type returned by the RenderingControl.GetMute is a list data type:

>>> mute = remote.RenderingControl.GetMute(0,'Master')
>>> print(mute)
[False]
>>> print(type(mute))
<class 'list'>
>>> print(mute[0])
False

Reviewing the code in ./upnp/init.py, the code to set mute is taking a Boolean and passing 'Enable' or 'Disable' to the UPNP functions based on the Boolean value:

    @mute.setter
    def mute(self, desired_mute):
        if not self.connected:
            return

        if desired_mute:
            desired_mute = 'Enable'
        else:
            desired_mute = 'Disable'
        try:
            self.MainTVAgent2.SetMute(desired_mute)
        except AttributeError:
            self.set_channel_mute('Master', desired_mute)

In the above, since 'MainTVAgent2.SetMute' fails with an AttributeError exception, set_channel_mute is called, passing in 'Enable' or 'Disable' (desired_mute variable). Set_channel_mute tries to pass the 'Enable' or 'Disable' value to the UPNP method RenderingControl.SetMute and this too fails as RenderingControl.SetMute is expecting a Boolean true/false instead of 'Enable' or 'Disable' resulting in an exception of "TypeError: Incorrect Value":

    def set_channel_mute(self, channel, desired_mute):
        if not self.connected:
            return

        self.RenderingControl.SetMute(0, channel, desired_mute)

The code to retrieve the current mute state is expecting the UPNP method to return 'Disable' and return a Boolean value of False, or True if the returned value is not 'Disable' (hence why remote.mute always returns True in my case):

    @property
    def mute(self):
        if not self.connected:
            return

        try:
            status = self.MainTVAgent2.GetMuteStatus()[1]
        except AttributeError:
            status = self.get_channel_mute('Master')

        if status == 'Disable':
            return False
        else:
            return True

By changing the code for the mute property in ./upnp/init.py to work with Boolean True/False values instead of string values 'Enable', 'Disable', as in the following example, remote.mute works as expected:

    @property
    def mute(self):
        if not self.connected:
            return

        try:
            status = self.MainTVAgent2.GetMuteStatus()[1]
        except AttributeError:
            status = self.get_channel_mute('Master')

        return status[0]

    @mute.setter
    def mute(self, desired_mute):
        if not self.connected:
            return

        try:
            self.MainTVAgent2.SetMute(desired_mute)
        except AttributeError:
            self.set_channel_mute('Master', desired_mute)
>>> print(remote.mute)
False
>>> remote.mute = True
>>> print(remote.mute)
True

However, I suspect these changes to the code would break the mute function on other TVs.

kdschlosser commented 5 years ago

ahh OK i see what is going on is going on..

calling the UPNP function directly is going to result in a list being returned. this is normal behavior.

I think that happened was I had coded some of the properties before I decide ot make a full on handler for the UPNP. I decided to handle all of this hind of stuff directly in the data type object i created instead of having a bunch of duplicate code all over the place. I must have missed changing the mute to work with the data type object returned values.

funny thing is I did modify it to work with either MainTVAgen2 as well as RenderingControl. so if one fails it will get it from the other location. at least i got that part right.. LOL

do you want to submit a PR for the code change? or would you like me to fix the problem and just add your name to the comments for the commit?

kdschlosser commented 5 years ago

also TY for spending the time in keying up a very detailed revision of what is going on.. was far easier to understand the second time around. also TY for providing a fix for the issue as well.

jgrieger1 commented 5 years ago

Go ahead and make the fix. I don't have a fork to pull from. I just discovered your updated samsungctl last night and you have improved it leaps and bounds from what it was. I though I was finally going to get full control of my TV via IP for better integration into my HA system. I've been playing around with it more today and unfortunately have discovered that with my TV missing the MainTVAgent2 service, I'm still lacking some of the controls I would like to have (primarily select_source). At least now I've got mute status and the ability to get and set the volume level instead of stepping up and down. These alone are significant, so thank you very much for the time and effort you have put into improving the samsungctl library. I'll be keeping an eye on your repo in hopes that a way to select source is found without requiring the MainTVAgent2 service.

kdschlosser commented 5 years ago

I am not sure of the model of the TV you have. since you mention the lack of the MainTVAgent2 II am going to think that you have a 2016 or newer TV.

and if that is the case. follow these instructions.

https://developer.samsung.com/tv/develop/extension-libraries/smart-view-sdk/receiver-apps/debugging

this is going to enable you to get communication logs from the TV. you are going to want to install the smartthings app connect the thing to your TV.. and poke about in the app. this is going to fill the log on the TV with all kinds of very helpful information. you never know maybe we can get the source changes to work over the websocket. I have been expanding the websocket functionality on a daily bases. I do not own a new Samsung TV so I am not able to do this personally.

attach the logs as a text file to a post so I can have a look see.

I did really expand the functionality of this library. it needed to be. Everyone bitches about the lack of ability to control the TV's. When in fact the ability to control them just like every other brand TV is there. we simply have to figure it out ourselves. Samsung sucks at documenting things.. And they are also extremely hush hush about their API because of what some ass did a few years back and published an article on how easy it was to activate the voice recognition on a TV and listen in to peoples conversations which i do not believe was actually done. I do believe that the voice recognition was activated. but they were not able to listen in. ever since then the API has been pretty hush hush this all started with the H and J series TV's

You should also have control of brightness, sharpness, color temperature, contrast, I did dink around with adding a few more websocket commands. I am not sure if they are operational or not.. the code is in my develop branch.