Skip to content

Instantly share code, notes, and snippets.

@emonkak
Last active June 7, 2021 03:23
Show Gist options
  • Save emonkak/e6e851b7ef1dce383155f8f5e2e0ae38 to your computer and use it in GitHub Desktop.
Save emonkak/e6e851b7ef1dce383155f8f5e2e0ae38 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# Reference:
# https://github.com/r45635/HVAC-IR-Control/blob/master/Protocol/Panasonic%20HVAC%20IR%20Protocol%20specification.pdf
import argparse
import enum
import pathlib
import subprocess
import sys
class Mode(enum.IntEnum):
auto = 0b0000
dry = 0b0010
cool = 0b0011
heat = 0b0100
fan = 0b0110
class Power(enum.IntEnum):
off = 0b0000
on = 0b0001
class Swing(enum.IntEnum):
p1 = 0b0001
p2 = 0b0010
p3 = 0b0011
p4 = 0b0100
p5 = 0b0101
auto = 0b1111
class Fan(enum.IntEnum):
f1 = 0b0011
f2 = 0b0100
f3 = 0b0101
f4 = 0b0110
f5 = 0b0111
auto = 0b1010
class Profile(enum.IntEnum):
normal = 0b0000_0000
quite = 0b0010_0000
boost = 0b0000_0001
class Features1(enum.IntFlag):
dry = 0b0100_0000
class Features2(enum.IntFlag):
deodorize = 0b0001_0000
def lsb_bits(byte):
for n in range(0, 8):
yield (byte & (1 << n)) != 0
def msb_bits(byte):
for n in range(7, -1, -1):
yield (byte & (1 << n)) != 0
def to_aeha_frame(bytes, to_bits):
yield [8, 4]
for byte in bytes:
for bit in to_bits(byte):
if bit:
yield [1, 3]
else:
yield [1, 1]
def create_panasonic_aircon_frames(temperature, mode, power, swing, fan, profile, features1, features2):
first_frame = b'\x40\x04\x07\x20\x00\x00\x00\x60'
yield from to_aeha_frame(first_frame, msb_bits)
second_frame = bytearray([
0x02, # 00
0x20, # 01
0xe0, # 02
0x04, # 03 (MODE[3] MODE[2] MODE[1] MODE[0] 1 0 0 POWER)
0x00, # 04
0x00, # 05
0x00, # 06 (0 0 1 TEMP[3] TEMP[2] TEMP[1] TEMP[0] 0)
0x80, # 07
0x00, # 08 (FAN[3] FAN[2] FAN[1] FAN[0] SWING[3] SWING[2] SWING[1] SWING[0])
0x00, # 09
0x00, # 10
0x06, # 11
0x60, # 12
0x00, # 13 (FEATURES1[1] FEATURES1[0] PROFILE[5] PROFILE[4] PROFILE[3] PROFILE[2] PROFILE[1] PROFILE[0])
0x00, # 14
0x80, # 15
0x00, # 16
0x06, # 17 (FEATURES2[3] FEATURES2[2] FEATURES2[1] FEATURES2[0] 1 0 1 0)
0x00, # 18 (CRC)
])
second_frame[5] = (mode << 4) | power
second_frame[6] = (((temperature & 0x0f) | 0x10) << 1)
second_frame[8] = (fan << 4) | swing
second_frame[13] = profile | features1
second_frame[17] |= features2
second_frame[18] = sum(second_frame) % 256
yield [1, 23] # Gap signal
yield from to_aeha_frame(second_frame, lsb_bits)
def emit_frames(handle, frames, signal_period):
for [pulse, space] in frames:
handle.write(f'pulse {pulse * signal_period}\n')
handle.write(f'space {space * signal_period}\n')
handle.write(f'pulse {signal_period}\n')
handle.flush()
if __name__ == '__main__':
DEFAULT_SIGNAL_PERIOD = 435
parser = argparse.ArgumentParser()
parser.add_argument('-d', '--device', nargs=1, type=pathlib.Path, default="/dev/lirc0")
parser.add_argument('-s', '--swing', choices=[v.name for v in Swing], default=Swing.auto.name)
parser.add_argument('-f', '--fan', choices=[v.name for v in Fan], default=Fan.auto.name)
parser.add_argument('-p', '--profile', choices=[v.name for v in Profile], default=Profile.normal.name)
parser.add_argument('--dry', action='store_true')
parser.add_argument('--deodorize', action='store_true')
parser.add_argument('--signal-period', type=int, default=DEFAULT_SIGNAL_PERIOD)
parser.add_argument('--no-send', action='store_true')
subparsers = parser.add_subparsers()
subparsers.required = True
subparsers.dest = 'command'
for v in Mode:
sub_parser = subparsers.add_parser(v.name, aliases=[v.name[0]])
sub_parser.add_argument('temperature', help='Temperature (16-30C)', type=int, choices=range(16, 31))
sub_parser.set_defaults(mode=v.name, power=Power.on.name)
sub_parser = subparsers.add_parser('off', aliases=['o'])
sub_parser.set_defaults(temperature=16, mode=Mode.auto.name, power=Power.off.name)
args = parser.parse_args()
temperature = args.temperature
mode = Mode[args.mode]
power = Power[args.power]
swing = Swing[args.swing]
fan = Fan[args.fan]
profile = Profile[args.profile]
features1 = 0
features2 = 0
signal_period = args.signal_period
if args.dry:
features1 |= Features1.dry
if args.deodorize:
features1 |= Features2.deodorize
frames = create_panasonic_aircon_frames(temperature, mode, power, swing, fan, profile, features1, features2)
if args.no_send:
emit_frames(sys.stdout, frames, signal_period)
else:
proc = subprocess.Popen(['ir-ctl', '--device', args.device, '--send', '/dev/stdin'], stdin=subprocess.PIPE, universal_newlines=True)
emit_frames(proc.stdin, frames, signal_period)
proc.stdin.close()
proc.wait()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment