Last active
June 7, 2021 03:23
-
-
Save emonkak/e6e851b7ef1dce383155f8f5e2e0ae38 to your computer and use it in GitHub Desktop.
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 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