cmer / lg-tv-control-macos

Automatically wake/sleep and change the input of your LG TV when used as a monitor on macOS
123 stars 12 forks source link

Issue with MacBook Pro M2 Max and LG C2 42" OLED TV #26

Closed NissarAK84 closed 7 months ago

NissarAK84 commented 8 months ago

Hi, I am having issues.

I followed the guidelines in the Readme section, and eventually got it to work, where the TV would turn off when my MacBook is put to sleep and would turn on again when the MacBook is woken. But it didn't work when I turned off my MacBook or on. I would have to turn the TV off / on manually on the remote.

After 1 day, even that stopped working.

I have been able to run the commands to turn the TV off / on through terminal however.

See my lgtv_init.lua code below. The only thing I added was my TV's actual name "TV Kontor", however, on the macOS Display Settings it is shown as "LG TV SSCR2":


local tv_input = "HDMI 1" -- Input to which your Mac is connected
local switch_input_on_wake = true -- Switch input to Mac when waking the TV
local prevent_sleep_when_using_other_input = true -- Prevent sleep when TV is set to other input (ie: you're watching Netflix and your Mac goes to sleep)
local debug = true -- If you run into issues, set to true to enable debug messages
local control_audio = true -- Control TV volume and mute button events from keyboard
local disable_lgtv = false
-- NOTE: You can disable this script by setting the above variable to true, or by creating a file named
-- `disable_lgtv` in the same directory as this file, or at ~/.disable_lgtv.

-- You likely will not need to change anything below this line
local tv_name = "MyTV" -- Name of your TV, set when you run `lgtv auth`
local connected_tv_identifiers = {"LG TV", "TV Kontor", "LG TV SSCR2"} -- Used to identify the TV when it's connected to this computer
local screen_off_command = "off" -- use "screenOff" to keep the TV on, but turn off the screen.
local lgtv_path = "~/opt/lgtv/bin/lgtv" -- Full path to lgtv executable
local lgtv_cmd = lgtv_path.." "..tv_name
local app_id = "com.webos.app."..tv_input:lower():gsub("_", "")
local lgtv_ssl = true -- Required for firmware 03.30.16 and up. Also requires LGWebOSRemote version 2023-01-27 or newer.

-- A convenience function for printing debug messages. 
function log_d(message)
  if debug then print(message) end
end

function lgtv_current_app_id()
  local foreground_app_info = exec_command("getForegroundAppInfo")
  for w in foreground_app_info:gmatch('%b{}') do
    if w:match('\"response\"') then
      local match = w:match('\"appId\"%s*:%s*\"([^\"]+)\"')
      if match then
        return match
      end
    end
  end
end

function tv_is_connected()
  for i, v in ipairs(connected_tv_identifiers) do
    if hs.screen.find(v) ~= nil then
      log_d(v.." is connected")
      return true
    end
  end

  log_d("No screens are connected. Please check the 'connected_tv_identifier' in the 'lgtv_init.lua' script matches your connected screen.")
  return false
end

function tv_is_current_audio_device()
  local current_audio_device = hs.audiodevice.current().name

  for i, v in ipairs(connected_tv_identifiers) do
    if current_audio_device == v then
      log_d(v.." is the current audio device")
      return true
    end
  end

  log_d(current_audio_device.." is the current audio device.")
  return false
end

function dump_table(o)
  if type(o) == 'table' then
    local s = '{ '
    for k,v in pairs(o) do
      if type(k) ~= 'number' then k = '"'..k..'"' end
      s = s .. '['..k..'] = ' .. dump_table(v) .. ','
    end
    return s .. '} '
  else
    return tostring(o)
  end
end

function exec_command(command)
  if lgtv_ssl then
    space_loc = command:find(" ")

    --- "ssl" must be the first argument for commands like 'startApp'. Advance it to the expected position.
    if space_loc then
      command = command:sub(1,space_loc).."ssl "..command:sub(space_loc+1)
    else
      command = command.." ssl"
    end
  end

  command = lgtv_cmd.." "..command
  log_d("Executing command: "..command)

  response = hs.execute(command)
  log_d(response)

  return response
end

-- Source: https://stackoverflow.com/a/4991602
function file_exists(name)
  local f=io.open(name,"r")
  if f~=nil then
    io.close(f)
    return true
  else
    return false
  end
end

function lgtv_disabled()
  return disable_lgtv or file_exists("./disable_lgtv") or file_exists(os.getenv('HOME') .. "/.disable_lgtv")
end

-- Converts an event_type (int) into a debug friendly description (string).
-- Source (look for `add_event_enum(lua_State* L)`): https://github.com/Hammerspoon/hammerspoon/blob/master/extensions/caffeinate/libcaffeinate_watcher.m
function event_type_description(event_type)
  if event_type == hs.caffeinate.watcher.systemDidWake then
    return "systemDidWake"
  elseif event_type == hs.caffeinate.watcher.systemWillSleep then
    return "systemWillSleep"
  elseif event_type == hs.caffeinate.watcher.systemWillPowerOff then
    return "systemWillPowerOff"
  elseif event_type == hs.caffeinate.watcher.screensDidSleep then
    return "screensDidSleep"
  elseif event_type == hs.caffeinate.watcher.screensDidWake then
    return "screensDidWake"
  elseif event_type == hs.caffeinate.watcher.sessionDidResignActive then
    return "sessionDidResignActive"
  elseif event_type == hs.caffeinate.watcher.sessionDidBecomeActive then
    return "sessionDidBecomeActive"
  elseif event_type == hs.caffeinate.watcher.screensaverDidStart then
    return "screensaverDidStart"
  elseif event_type == hs.caffeinate.watcher.screensaverWillStop then
    return "screensaverWillStop"
  elseif event_type == hs.caffeinate.watcher.screensaverDidStop then
    return "screensaverDidStop"
  elseif event_type == hs.caffeinate.watcher.screensDidLock then
    return "screensDidLock"
  elseif event_type == hs.caffeinate.watcher.screensDidUnlock then
    return "screensDidUnlock"
  else
    return "unknown"
  end
end

if debug then
  log_d("TV name: "..tv_name)
  log_d("TV input: "..tv_input)
  log_d("LGTV path: "..lgtv_path)
  log_d("LGTV command: "..lgtv_cmd)
  log_d("SSL: "..tostring(lgtv_ssl))
  log_d("App ID: "..app_id)
  log_d("lgtv_disabled: "..tostring(lgtv_disabled()))
  if not lgtv_disabled() then
    exec_command("swInfo")
    exec_command("getForegroundAppInfo")
    log_d("Connected screens: "..dump_table(hs.screen.allScreens()))
    log_d("TV is connected? "..tostring(tv_is_connected()))
  end
end

watcher = hs.caffeinate.watcher.new(
  function(event_type)
    event_name = event_type_description(event_type)
    log_d("Received event: "..(event_type or "").." "..(event_name))

    if lgtv_disabled() then
      log_d("LGTV feature disabled. Skipping.")
      return
    end

    if (event_type == hs.caffeinate.watcher.screensDidWake or
        event_type == hs.caffeinate.watcher.systemDidWake or
        event_type == hs.caffeinate.watcher.screensDidUnlock) then

      if not tv_is_connected() then
        log_d("TV was not turned on because it is not connected")
        return
      end

      if screen_off_command == 'screenOff' then
        exec_command("screenOn") -- turn on screen
      else
        exec_command("on") -- wake on lan
      end
      log_d("TV was turned on")

      if lgtv_current_app_id() ~= app_id and switch_input_on_wake then
        exec_command("startApp "..app_id)
        log_d("TV input switched to "..app_id)
      end
    end

    if (event_type == hs.caffeinate.watcher.screensDidSleep or 
        event_type == hs.caffeinate.watcher.systemWillPowerOff or 
        event_type == hs.caffeinate.watcher.systemWillSleep) then

      if not tv_is_connected() then
        log_d("TV was not turned off because it is not connected")
        return
      end

      -- current_app_id returns empty string on some events like screensDidSleep
      current_app_id = lgtv_current_app_id()
      if current_app_id ~= app_id and prevent_sleep_when_using_other_input then
        log_d("TV is currently on another input ("..(current_app_id or "?").."). Skipping powering off.")
        return
      else
        exec_command(screen_off_command)
        log_d("TV screen was turned off with command `"..screen_off_command.."`.")
      end
    else 
      log_d("Event ignored")
    end
  end
)

tap = hs.eventtap.new({ hs.eventtap.event.types.keyDown, hs.eventtap.event.types.systemDefined }, function(event)
  local event_type = event:getType()
  if event_type ~= hs.eventtap.event.types.systemDefined or not tv_is_current_audio_device() then
    return
  end

  local system_key = event:systemKey()

  local keys_to_commands = {['SOUND_UP']="volumeUp", ['SOUND_DOWN']="volumeDown"}
  local pressed_key = system_key.key

  if system_key.down and (pressed_key == 'MUTE' or keys_to_commands[pressed_key] ~= nil) then
    if pressed_key == 'MUTE' then
      local audio_status = hs.json.decode(exec_command("audioStatus"):gmatch('%b{}')())
      local mute_status = audio_status["payload"]["mute"]
      exec_command("mute "..tostring(not mute_status))
    else
      exec_command(keys_to_commands[pressed_key])
    end
  end
end)

watcher:start()

if control_audio then
  tap:start()
end

Here is the log from Hammerspoon Console:


2023-11-12 23:53:33: -- Lazy extension loading enabled
2023-11-12 23:53:33: -- Loading ~/.hammerspoon/init.lua
2023-11-12 23:53:33: TV name: MyTV
2023-11-12 23:53:33: TV input: HDMI 1
2023-11-12 23:53:33: LGTV path: ~/opt/lgtv/bin/lgtv
2023-11-12 23:53:33: LGTV command: ~/opt/lgtv/bin/lgtv MyTV
2023-11-12 23:53:33: SSL: true
2023-11-12 23:53:33: App ID: com.webos.app.hdmi 1
2023-11-12 23:53:33: lgtv_disabled: false
2023-11-12 23:53:33: Executing command: ~/opt/lgtv/bin/lgtv MyTV swInfo ssl
2023-11-12 23:53:34: {"type": "response", "id": "sw_info_0", "payload": {"returnValue": true, "product_name": "webOSTV 7.0", "model_name": "HE_DTV_W22O_AFABATAA", "sw_type": "FIRMWARE", "major_ver": "03", "minor_ver": "33.85", "country": "DK", "country_group": "EU", "device_id": "74:e6:b8:ef:dd:e5", "auth_flag": "N", "ignore_disable": "N", "eco_info": "01", "config_key": "00", "language_code": "en-GB"}}
{"closing": {"code": 1000, "reason": ""}}

2023-11-12 23:53:34: Executing command: ~/opt/lgtv/bin/lgtv MyTV getForegroundAppInfo ssl
2023-11-12 23:53:34: {"type": "response", "id": "0", "payload": {"returnValue": true, "appId": "com.webos.app.hdmi1", "processId": "", "windowId": ""}}
{"closing": {"code": 1000, "reason": ""}}

2023-11-12 23:53:34: -- Loading extension: screen
2023-11-12 23:53:34: Connected screens: { [1] = hs.screen: LG TV SSCR2 (0x600003621578),[2] = hs.screen: Built-in Retina Display (0x600003620f38),} 
2023-11-12 23:53:34: LG TV is connected
2023-11-12 23:53:34: TV is connected? true
2023-11-12 23:53:34: -- Loading extension: caffeinate
2023-11-12 23:53:34: -- Loading extension: eventtap
2023-11-12 23:53:34: -- Done.
2023-11-12 23:53:53: -- Loading extension: audiodevice
2023-11-12 23:53:53: LG TV SSCR2 (eqMac) is the current audio device.

Can anybody please advise on what is wrong? Currently nothing works at all.

Krizid commented 7 months ago

what worked for me was changing the SSL command and removing MyTV name. Backup your init file before trying this. Reload config and set lockscreen to 1min. if should shutdown your LG TV. If you have issues waking the TV up. in your TV settings enable Always Ready.

command = command.." ssl"
change that to command = command.." --ssl"

another one you might wanna try with that if that command doesn't work is local tv_name = "MyTV" change to local tv_name = ""

NissarAK84 commented 7 months ago

what worked for me was changing the SSL command and removing MyTV name. Backup your init file before trying this. Reload config and set lockscreen to 1min. if should shutdown your LG TV. If you have issues waking the TV up. in your TV settings enable Always Ready.

command = command.." ssl" change that to command = command.." --ssl"

another one you might wanna try with that if that command doesn't work is local tv_name = "MyTV" change to local tv_name = ""

Hi @Krizid, Thanks for your response, I thought I would never hear from anyone :(

I tried first with changing the part of the code that says:

function exec_command(command)
  if lgtv_ssl then
    space_loc = command:find(" ")

    --- "ssl" must be the first argument for commands like 'startApp'. Advance it to the expected position.
    if space_loc then
      command = command:sub(1,space_loc).."ssl "..command:sub(space_loc+1)
    else
      command = command.." --ssl"
    end
  end

  command = lgtv_cmd.." "..command
  log_d("Executing command: "..command)

  response = hs.execute(command)
  log_d(response)

  return response
end

to:

function exec_command(command)
  if lgtv_ssl then
    space_loc = command:find(" ")

    --- "ssl" must be the first argument for commands like 'startApp'. Advance it to the expected position.
    if space_loc then
      command = command:sub(1,space_loc).."ssl "..command:sub(space_loc+1)
    else
      command = command.." --ssl"
    end

Basically added the two dashes before command.." ssl".

It didn't work. Then I changed this part:

local tv_name = "MyTV" -- Name of your TV, set when you run `lgtv auth``

to:

local tv_name = "" -- Name of your TV, set when you run `lgtv auth``

Basically removed MyTV.

This didn't work either.

I also tried with only changing the MyTV part and leaving the ssl without the dashes, no luck.

With the " --ssl" I get this output in the log:

2023-12-07 14:06:40: Executing command: ~/opt/lgtv/bin/lgtv  getForegroundAppInfo --ssl
2023-12-07 14:06:40: Error: '--ssl'
LGTV Controller
Author: Karl Lattimer <karl@qdh.org.uk>
Usage: lgtv <command> [parameter]

Available Commands:
  -i                    interactive mode
  scan
  auth <host> <tv_name>
  <tv_name> audioStatus <>
  <tv_name> audioVolume <>
  <tv_name> closeAlert <alertId>
  <tv_name> closeApp <appid>
  <tv_name> createAlert <message> <button>
  <tv_name> execute <command>
  <tv_name> getCursorSocket <>
  <tv_name> getForegroundAppInfo <>
  <tv_name> getPictureSettings <>
  <tv_name> getPowerState <>
  <tv_name> getSoundOutput <>
  <tv_name> getSystemInfo <>
  <tv_name> getTVChannel <>
  <tv_name> input3DOff <>
  <tv_name> input3DOn <>
  <tv_name> inputChannelDown <>
  <tv_name> inputChannelUp <>
  <tv_name> inputMediaFastForward <>
  <tv_name> inputMediaPause <>
  <tv_name> inputMediaPlay <>
  <tv_name> inputMediaRewind <>
  <tv_name> inputMediaStop <>
  <tv_name> listApps <>
  <tv_name> listChannels <>
  <tv_name> listInputs <>
  <tv_name> listLaunchPoints <>
  <tv_name> listServices <>
  <tv_name> mute <muted>
  <tv_name> notification <message>
  <tv_name> notificationWithIcon <message> <url>
  <tv_name> off
  <tv_name> on
  <tv_name> openAppWithPayload <payload>
  <tv_name> openBrowserAt <url>
  <tv_name> openYoutubeId <videoid>
  <tv_name> openYoutubeLegacyId <videoid>
  <tv_name> openYoutubeLegacyURL <url>
  <tv_name> openYoutubeURL <url>
  <tv_name> screenOff
  <tv_name> screenOn
  <tv_name> serialise
  <tv_name> setInput <input_id>
  <tv_name> setSoundOutput <output>
  <tv_name> setTVChannel <channel>
  <tv_name> setVolume <level>
  <tv_name> startApp <appid>
  <tv_name> swInfo <>
  <tv_name> volumeDown <>
  <tv_name> volumeUp <>

2023-12-07 14:06:40: -- Loading extension: screen
2023-12-07 14:06:40: Connected screens: { [1] = hs.screen: LG TV SSCR2 (0x6000012efd78),[2] = hs.screen: Built-in Retina Display (0x6000012efd38),} 
2023-12-07 14:06:40: LG TV is connected
2023-12-07 14:06:40: TV is connected? true
2023-12-07 14:06:40: -- Loading extension: caffeinate
2023-12-07 14:06:40: -- Loading extension: eventtap
2023-12-07 14:06:40: -- Done.
2023-12-07 14:06:53: -- Loading extension: audiodevice
2023-12-07 14:06:53: LG TV SSCR2 (eqMac) is the current audio device.

Finally, I don't know what you mean with Lockscreen should be changed to 1 minute: Screen Shot 2023-12-07 at 14 15 57 PM

NissarAK84 commented 7 months ago

@cmer Do you have an idea what is wrong here?

cmer commented 7 months ago

I just updated the script to work with the latest version of LGWebOSRemote. Reinstall everything by following the new instructions in README.md. It should solve your problem.

NissarAK84 commented 6 months ago

I just updated the script to work with the latest version of LGWebOSRemote. Reinstall everything by following the new instructions in README.md. It should solve your problem.

Hi @cmer I ended up doing a clean install on my Mac (it is relatively new, so I didn't mind). Now it all works perfectly.

I read your post on Reddit, and just want to make sure, this tweak does not apply when the MacBook is shut down, and only applies when you put the laptop to sleep, right?