samsung-tv-ws-api icon indicating copy to clipboard operation
samsung-tv-ws-api copied to clipboard

Feature Requests / Suggestions

Open gvitzi opened this issue 4 years ago • 11 comments

This library works great with my Samsung The Frame TV, good job! I'm also following the process of integrating it into Home-Assistant.

I have a few ideas of features I think are worth implementing, some I know that can be done (implemented on some other nodejs libs), some are suggestions / ideas that might or might not be possible.

I can contribute some code, but want to first make sure this is something you think should be added.

Things that work: (at least on my TV)

  1. get device info this can be done with: res = requests.get('http://{ip}:8001/api/v2/'.format(ip=self.host)) it will print device name, model, uuid, supported features and more this can be used to make model specific logic
  2. open YouTube with a specific video id (similar to open_url function)
  3. hold key (for a certain amount of time) can be done with a Press event > sleep > Release event - this can be used to implement power off for TV 'The Frame' since a short POWER click turns on 'Art Mode' and only if you hold the key for 3+ seconds it will completely turn off
  4. move mouse cursor (I don't see a lot of use for this personally, but I've seen it implemented on other libs and in the future it is possible to implement a mouse control pad for example in Home-Assistant)

Things I think are missing, but i'm not sure how or if it is possible to get them from the TV API: 5. get current source / app 6. get current volume level 7. fetch the icon images from the app icons url - the Home-Assistant media_player has the ability to show the images, if we could fetch them. it is working for the LG WebOS integration.

Similar libraries written in NodeJS:

  1. https://github.com/balmli/com.samsung.smart (samsung.js)
  2. https://github.com/tavicu/homebridge-samsung-tizen (remote.js)

let me know if you want me to open pull requests on any of the 'things that work'

gvitzi avatar Feb 17 '20 20:02 gvitzi

Hi! Thank you for the interest in the project!

I accept any PR that adds functionalities referring to the endpoint /api/v2 Samsung does not give documentation of all the functionalities that are available, so it would be prudent to work only with that endpoint and with DLNA. All features should be present on all Tizen2+ TVs.

I reply on the points you comment

  • get device info I would accept this

  • open YouTube with a specific video id (similar to open_url function) As Samsung being an idiot and not giving documentation, it is not known what will happen with this endpoint or what TV models it is. I don't know if I would integrate interactions with the old endpoints. Maybe it's not a bad idea but I'm not really convinced

  • hold key I would accept this

  • move mouse cursor I would accept this

  • get current source / app This could be done by DLNA/UPnP

  • get current volume level This could be done by DLNA/UPnP

  • fetch the icon images from the app icons url Maybe it can be done using /api/v2

Originally my roadmap was to make a second library to handle DLNA and through that I can use everything that is multimedia. But unfortunately I did not get to do it

xchwarze avatar Feb 17 '20 23:02 xchwarze

Great, it's good to know it's possible to get current state using DLNA, I will try to test it. I'm also trying to download all the Tizen OS sources with hope to maybe find out more available commands / routes.

I created 2 pull requests and will do the 3rd one soon, maybe tomorrow.

gvitzi avatar Feb 18 '20 20:02 gvitzi

This is Tizen's repository https://git.tizen.org/cgit/ There would have to be all the commands that can be used.

xchwarze avatar Feb 18 '20 21:02 xchwarze

Hey,

while working on the move_cursor feature, I created the following example for testing, it is an example of server and web page with a mouse control that will move the cursor on the TV. It is actually very responsive and almost feels as if the mouse is connected directly to the TV.

This could be added as a usage example to the project, but I'm not sure it is worth a pull request, so I'm posting it here.

** On my TV only the web browser app actually has a cursor moving. In the main menu the cursor does not appear.

ezgif-6-4028fa175776

mouse_server.py:

from samsungtvws.remote import SamsungTVWS
import os
import websockets
import asyncio
import json
import concurrent
from samsungtvws.exceptions import HttpApiError

TV_IP='192.168.0.X'

token_file = os.path.dirname(os.path.realpath(__file__)) + '/tv-token.txt'
tv = SamsungTVWS(host=TV_IP, port=8002, token_file=token_file)

loop = asyncio.get_event_loop()
executor = concurrent.futures.ThreadPoolExecutor(max_workers=3)

def move_tv_cursor(move_event):
    print("moving: {}".format(move_event))
    tv.move_cursor(move_event['x'], move_event['y'])

def send_click_to_tv(event):
    key = event['key']
    print("Clicked {}".format(key))
    tv.send_key(key)

async def accept_client(websocket, path):
    print("New Web Client Connected")
    await websocket.send("Connected To TV<br/>Use the mouse to drag the ball around")
    while True:
        msg = await websocket.recv()
        event = json.loads(msg)
        if event['type'] == 'move':
            await loop.run_in_executor(executor, move_tv_cursor, event)
        elif event['type'] == 'click':
            await loop.run_in_executor(executor, send_click_to_tv, event)

start_server = websockets.serve(accept_client, "localhost", 8888)

loop.run_until_complete(start_server)
loop.run_forever()

mouse_web_page.html:

<style>
    :root {
        --ball-diameter: 50px;
    }

    .container {
        position: relative;
        left: calc(50% - 200px);
        top: calc(50% - 200px);
        width: 400px;
        height: 400px;
        text-align: center;
        vertical-align: middle;
        font-family: consolas;
    }

    #msgbox {
        margin: auto;
    }

    .track-pad {
            position: relative;
            height: 90%;
            width: 90%;
            left: 5%;
            top: 5%;
            border-radius: calc(var(--ball-diameter) / 2);
            background: grey;
            -moz-box-shadow:    inset 0 0 10px #000000;
            -webkit-box-shadow: inset 0 0 10px #000000;
            box-shadow:         inset 0 0 10px #000000;
    }

    #ball {
        position: relative;
        top: calc(50% - var(--ball-diameter) / 2);
        left: calc(50% - var(--ball-diameter) / 2);
        width: var(--ball-diameter);
        height: var(--ball-diameter);
        border-radius: calc(var(--ball-diameter) / 2);
        background: black;
        background: radial-gradient(circle at 100px 100px, #fff, #000);
        cursor: grab;
    }

    #ball:active {
        background: radial-gradient(circle at 100px 100px, #0000ff, #000);
    }

    #enter {
        margin-top:30px;
        position: relative;
        width: 300px;
        left: 50px;
        height: 50px;
        border-radius: 10px;
        border: 2px solid #4CAF50;
        cursor: pointer;
    }

    #enter:hover {
        background-color: #4CAF50; 
    }

    #enter:active {
        background-color: #4CAF50;
        box-shadow: inset 0 0 10px; 
    }

    #enter div {
        position: relative;
        top: 15px;
    }
</style>

<div class="container">
    <h2 id="msgbox">Waiting for connection to TV</h2>
    <div class="track-pad">
        <div id="ball"></div>        
    </div>
    <div id="enter"><div>ENTER</div></div>
</div>

<script>
    const multiplier = 5;

    let ws = new WebSocket("ws://localhost:8888");
    ws.onopen = function(e) {
        console.log("Connection established");
    };

    ws.onclose = function(e) {
        console.log("Connection Lost");
        document.getElementById("msgbox").innerHTML = "Connection Lost </br>Refresh the page to try again";
    }

    ws.onmessage = function(event) {
        document.getElementById("msgbox").innerHTML = event.data;
        draggable(document.getElementById("ball"));

        document.getElementById("enter").onclick = function(e) {
            sendClickEvent();
        }
    }

    function sendMoveEvent(x, y) {
        event = { type: 'move', x: x, y: y };
        if (ws.readyState == WebSocket.OPEN)
            ws.send(JSON.stringify(event));
    }  

    function sendClickEvent() {
        event = { type: 'click', key: 'KEY_ENTER' };
        if (ws.readyState == WebSocket.OPEN)
            ws.send(JSON.stringify(event));
    }  
  
    function draggable(elmnt) {
        var startX = 0;
        var startY = 0;
        var centerX = elmnt.parentElement.offsetWidth / 2 - elmnt.offsetWidth / 2;
        var centerY = elmnt.parentElement.offsetHeight / 2 - elmnt.offsetHeight / 2;

        elmnt.onmousedown = onMouseDown;

        function onMouseDown(e) {
            e = e || window.event;
            e.preventDefault();
            // get the mouse cursor position at startup:
            startX = e.clientX;
            startY = e.clientY;
            document.onmouseup = closeDragElement;
            // call a function whenever the cursor moves:
            document.onmousemove = drag;
        }

        function drag(e) {
            e = e || window.event;
            e.preventDefault();
            // calculate the new cursor position:
            x = startX - e.clientX;
            y = startY - e.clientY;
            startX = e.clientX;
            startY = e.clientY;

            var dragX = elmnt.offsetLeft - x;
            var dragY = elmnt.offsetTop - y;

            // keep ball inside parent
            var rightBoundary = elmnt.parentElement.offsetWidth - elmnt.offsetWidth;
            var bottomBoundary = elmnt.parentElement.offsetHeight - elmnt.offsetHeight;

            var dragX = Math.min(Math.max(dragX, 0), rightBoundary);
            var dragY = Math.min(Math.max(dragY, 0), bottomBoundary);

            // set the element's new position:
            elmnt.style.left = dragX + "px";
            elmnt.style.top = dragY + "px";

            sendMoveEvent(-x * multiplier, -y * multiplier)
        }

        function closeDragElement() {
            // stop moving when mouse button is released:
            document.onmouseup = null;
            document.onmousemove = null;
            returnToCenter();
        }

        function returnToCenter() {
            elmnt.style.top = centerY + "px";
            elmnt.style.left = centerY + "px";
        }
    }

</script>

gvitzi avatar Feb 20 '20 19:02 gvitzi

Please add this in "docs/mouse example" and make a PR

xchwarze avatar Feb 20 '20 22:02 xchwarze

@xchwarze, so you're not planning to add DLNA/UPnP features to this library? My I ask you why? Speaking about home official HA integration (that's almost ready) it will allow to easily add the SUPPORT_VOLUME_SET feature. If you want I can make a PR. Just let me know. Thanks.

tulindo avatar Feb 28 '20 15:02 tulindo

I accept any PR that adds functionalities referring to the endpoint /api/v2

this would be the explanation

I was originally going to create a second library for that (all media functions), but I think that can be done with this: https://github.com/opacam/Cohen3

xchwarze avatar Feb 28 '20 20:02 xchwarze

I see. Main problem is that AFAIK cohen3 (that, after a brief look, I see as a generic communication library... Just like requests) will not be accepted in HA. They want a component like yours that hides low level details. Maybe cohen3 is something more?

tulindo avatar Feb 29 '20 11:02 tulindo

Is there a possibility to open Spotify with a certain playlist?

piNp187 avatar Sep 18 '20 15:09 piNp187

Great work getting this working for the newer TVs. It's actually ridiculous how amateur Samsung is compared to a company like Denon that publishes real docs about their API.

  • get current source / app This could be done by DLNA/UPnP
  • get current volume level This could be done by DLNA/UPnP

Do we have any examples online of using dlna/upnp to get or change the input source?

jtexp avatar Dec 01 '20 23:12 jtexp

@jtexp sure! check this sources https://github.com/xchwarze/ha-samsungtv-custom/pull/1

xchwarze avatar Dec 07 '20 01:12 xchwarze