Last active
March 2, 2024 17:46
-
-
Save alferz/38b6027c070ed0f2865baf2fd3a9de57 to your computer and use it in GitHub Desktop.
LG Air Conditioner Infrared Control Python Script with pigpio
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# lgairctrl.py | |
# 2024-03-01 | |
# Public Domain | |
""" | |
A utility to send an infrared command to certain, unknown models of LG Air Conditioners equipped with infrared. This utility currently supports the United States model LW1022IVSM with Fahrenheit temperature control (and probably many others). This script is meant to be run on a Raspberry Pi with pigpiod installed and an infrared LED module, or a bare IR LED with NPN transistor base connected to one of the GPIO pins. | |
To send a command use the following format. All options are required. | |
./lgairctrl.py -g <gpionumber> -m <mode> -t <temp> -f <fanspeed> | |
where mode is one of | |
o -- off | |
m -- max cool | |
e -- energy saver | |
s -- sleep mode (only works when unit is on. Send 'm' first, then 's' to enable sleep mode) | |
c -- clear sleep mode (puts unit back in previous mode, ie 'm') | |
f -- fan only | |
d -- dry (dehumidify) | |
temp is a number between and including 60 and 86 (fahrenheit) | |
and fanspeed is one of | |
1 -- low | |
2 -- med | |
3 -- high | |
""" | |
import time | |
import json | |
import os | |
import argparse | |
import pigpio # http://abyz.co.uk/rpi/pigpio/python.html | |
p = argparse.ArgumentParser() | |
p.add_argument("-g", "--gpio", help="GPIO for TX", required=True, type=int) | |
p.add_argument("-m", "--mode", help="o/off, m/max, e/enrgy svr, s/sleep, c/clr sleep, f/fan, d/dry", required=True) | |
p.add_argument("-t", "--temp", help="60-86 in f", required=True) | |
p.add_argument("-f", "--fan", help="1/lo 2/md 3/hi", required=True) | |
p.add_argument("-v", "--verbose", help="Be verbose", action="store_true") | |
args = p.parse_args() | |
GPIO = args.gpio | |
MODE = args.mode | |
TEMP = args.temp | |
FAN = args.fan | |
VERBOSE = args.verbose | |
FREQ = 38.0 | |
GAP_MS = 100 | |
GAP_S = GAP_MS / 1000.0 | |
def printCmd(cmd): | |
outstring = """FXD FXD SPCL MODE TEMP FANS CKSM\n""" | |
for i, v in enumerate(cmd): | |
outstring += str(v) | |
if (i+1)%4 == 0: | |
outstring += " " | |
print(outstring) | |
def getChecksum(cmd): #LG checksum is the sum of each 4 bit group, discarding binary overflow (ie last 4 least significant bits) | |
totalSum = 0 | |
for i in range(0, len(cmd), 4): | |
sum4 = cmd[i]*8 + cmd[i+1]*4 + cmd[i+2]*2 + cmd[i+3]*1 | |
totalSum += sum4 | |
cksumArr = [int(x) for x in bin(totalSum%16)[2:]] #Get array of least 4 significant bits by modding by 16 | |
for i in range (0, 4-len(cksumArr)): #Pad array out to 4 digits with leading zeroes | |
cksumArr = [0] + cksumArr | |
if VERBOSE: | |
print('checksum array: ' + str(cksumArr)) | |
return cksumArr | |
def carrier(gpio, frequency, micros): | |
""" | |
Generate carrier square wave. | |
""" | |
wf = [] | |
cycle = 1000.0 / frequency | |
cycles = int(round(micros/cycle)) | |
on = int(round(cycle / 2.0)) | |
sofar = 0 | |
for c in range(cycles): | |
target = int(round((c+1)*cycle)) | |
sofar += on | |
off = target - sofar | |
sofar += off | |
wf.append(pigpio.pulse(1<<gpio, 0, on)) | |
wf.append(pigpio.pulse(0, 1<<gpio, off)) | |
return wf | |
def xmitcmd(cmd): | |
pi = pigpio.pi() # Connect to Pi. | |
pi.wave_clear() #Clear any previous stored waveforms | |
if not pi.connected: | |
print('Fatal error, could not connect to pigpiod!') | |
exit(0) | |
pi.set_mode(GPIO, pigpio.OUTPUT) # IR TX connected to this GPIO. | |
pi.wave_add_new() | |
emit_time = time.time() | |
if VERBOSE: | |
print("Playing") | |
# Create wave, twice as many as the cmd plus 2 preamble bits and 1 ending bit | |
wave = [0]*((len(cmd)*2)+3) #Add two bits for the preamble and one for ending bit | |
#First add the preamble mark/space | |
wf = carrier(GPIO, FREQ, 3122) #Preamble mark is 3122 | |
pi.wave_add_generic(wf) | |
#print('add mark 3122') | |
wave[0] = pi.wave_create() | |
pi.wave_add_generic([pigpio.pulse(0, 0, 9661)]) #Preamble space is 9661 | |
#print('add space 9661') | |
wave[1] = pi.wave_create() | |
pi.wave_add_generic([pigpio.pulse(0, 0, 507)]) #Short space is a pulse of 507 | |
SPACE_SHORT = pi.wave_create() | |
pi.wave_add_generic([pigpio.pulse(0, 0, 1542)]) #Long space is a pulse of 1542 | |
SPACE_LONG = pi.wave_create() | |
wf = carrier(GPIO, FREQ, 509) #Mark is a pulse of 509 | |
pi.wave_add_generic(wf) | |
MARK = pi.wave_create() | |
for i in range(0, len(cmd)): | |
wave_index = i*2 + 2 | |
wave[wave_index] = MARK | |
if(cmd[i] == 0): | |
wave[wave_index+1] = SPACE_SHORT | |
else: | |
wave[wave_index+1] = SPACE_LONG | |
#Add ending MARK bit | |
wave[len(wave)-1] = MARK | |
delay = emit_time - time.time() | |
if delay > 0.0: | |
time.sleep(delay) | |
pi.wave_chain(wave) #This actually sends the entire command | |
while pi.wave_tx_busy(): | |
time.sleep(0.002) | |
emit_time = time.time() + GAP_S | |
pi.stop() # Disconnect from Pi. | |
code_parts = { | |
'FXD1': [1,0,0,0], #This part never changes for LG | |
'FXD2': [1,0,0,0], #This part never changes for LG | |
'SPCL': [1,1,0,0], #This is the default for the off command | |
'MODE': [0,0,0,0], #This is the default for the off command | |
'TEMP': [0,0,0,0], #This is the default for the off command | |
'FANS': [0,1,0,1] #This is the default for the off command | |
} | |
if(args.mode == 'o'): | |
#This is the off cmd, which is already preconfigured in code_parts. Send it as is. | |
print('Sending command: off') | |
else: | |
resText = 'Sending command: ' | |
code_parts['SPCL'] = [0,0,0,0] | |
tempMap = { #digits 1-4 are the values in the TEMP array, the 5th digit goes in position 2/idx1 in SPCL as a kicker | |
'60': [0,0,0,1,0], | |
'61': [0,0,0,1,1], | |
'62': [0,0,1,0,0], | |
'63': [0,0,1,0,1], | |
'64': [0,0,1,1,0], | |
'65': [0,0,1,1,1], | |
'66': [0,1,0,0,0], | |
'67': [0,1,0,0,1], | |
'68': [0,1,0,1,0], | |
'69': [0,1,0,1,1], | |
'70': [0,1,1,0,0], | |
'71': [0,1,1,0,1], | |
'72': [0,1,1,1,0], | |
'73': [0,1,1,1,1], | |
'74': [1,0,0,0,0], | |
'75': [1,0,0,0,1], | |
'76': [1,0,0,1,0], | |
'77': [1,0,1,0,0], | |
'78': [1,0,1,1,0], | |
'79': [1,0,1,1,1], | |
'80': [1,1,0,0,0], | |
'81': [1,1,0,0,1], | |
'82': [1,1,0,1,0], | |
'83': [1,1,0,1,1], | |
'84': [1,1,1,0,0], | |
'85': [1,1,1,0,1], | |
'86': [1,1,1,1,0] | |
} | |
code_parts['TEMP'] = tempMap[args.temp][0:4] | |
code_parts['SPCL'][1] = tempMap[args.temp][4] | |
resText += args.temp + 'f / ' | |
match args.mode: #m, e, s, c, f, d | |
case 'm': | |
code_parts['MODE'] = [0,0,0,0] | |
resText += 'max cool / ' | |
case 'e': | |
code_parts['MODE'] = [0,1,1,0] | |
resText += 'energy save / ' | |
case 's': | |
code_parts['SPCL'] = [1,0,1,0] | |
code_parts['MODE'] = [0,0,0,0] | |
resText += 'sleep mode / ' | |
case 'c': | |
code_parts['SPCL'] = [1,0,1,1] | |
code_parts['MODE'] = [0,0,0,0] | |
resText += 'clear sleep mode / ' | |
case 'f': | |
code_parts['MODE'] = [0,1,0,1] | |
resText += 'fan only / ' | |
case 'd': | |
code_parts['MODE'] = [0,0,0,1] | |
resText += 'dry mode / ' | |
match args.fan: | |
case '1': | |
code_parts['FANS'] = [0,0,0,0] | |
resText += 'low fan' | |
case '2': | |
code_parts['FANS'] = [0,0,1,0] | |
resText += 'med fan' | |
case '3': | |
code_parts['FANS'] = [0,1,0,0] | |
resText += 'hi fan' | |
print(resText) | |
cmd = [] | |
for i, (k,v) in enumerate(code_parts.items()): | |
cmd += v | |
cmd += getChecksum(cmd) #Add the checksum | |
printCmd(cmd) | |
xmitcmd(cmd) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment