Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- """
- AP700 write-only PyScript using older APK default calibration rows.
- What this version does:
- - keeps the known-good AP700 TCP framing/checksum
- - uses SET_ALL_CHANNEL_INDEX only
- - uses the older APK's built-in default calibration rows (0..53) for named colors
- - computes channels from angle using the app-style color-region table
- - uses user-facing intensity 0..100
- - does NO live reads
- Install:
- Save as /config/pyscript/ap700_default_cal.py
- Reload PyScript
- Services:
- pyscript.ap700_on
- pyscript.ap700_off
- pyscript.ap700_power
- pyscript.ap700_find_me
- pyscript.ap700_send_hex
- pyscript.ap700_set_channels12
- pyscript.ap700_set_calibration_row
- pyscript.ap700_set_angle
- pyscript.ap700_set_color_name
- pyscript.ap700_color_names
- """
- import socket
- LIGHT_HOSTS = ["10.50.52.2", "10.50.52.3"]
- PORT = 8899
- TIMEOUT = 0.7
- START_BYTE = 0xA9
- STOP_BYTE = 0x5C
- CMD_SET_ALL_CHANNEL_INDEX = 0x01
- CMD_POWER = 0x27
- CMD_FIND_ME = 0x76
- # Older APK built-in default calibration rows 0..53.
- CALIBRATION = [
- [720,3028,720,4095,4095,0,670,3632,670,3913,3913,0],
- [0,2318,0,2060,2060,0,0,2250,0,2000,2000,0],
- [0,807,0,0,1421,0,0,800,0,0,1400,0],
- [1100,2813,1100,4095,4095,0,1084,2746,1084,4034,4034,0],
- [821,2144,821,1870,2082,0,847,2118,847,1857,2056,0],
- [0,1255,731,794,786,0,0,1236,721,772,773,0],
- [1660,1704,1660,4095,4095,0,1626,1743,1626,4095,4095,0],
- [1071,1953,1164,1829,1878,0,1071,1953,1164,1829,1878,0],
- [724,1212,724,759,805,0,0,1178,875,751,746,0],
- [2223,1000,2223,3568,3508,0,2200,1635,2200,4095,2734,0],
- [1405,1684,1405,1720,1684,0,1405,1684,1405,1720,1684,0],
- [788,986,788,0,986,0,786,961,786,0,961,0],
- [2684,703,2684,3486,2955,0,2695,985,2695,3181,2734,0],
- [1676,1676,1676,1390,1502,0,1654,1698,1654,1401,1524,0],
- [832,913,832,0,913,0,826,903,826,0,898,0],
- [3161,770,3161,4054,1833,0,3333,1915,3333,2069,1967,0],
- [1918,1429,1918,1259,1314,0,1922,1451,1922,1257,1323,0],
- [885,857,885,0,857,0,884,852,884,0,842,0],
- [4015,1125,4015,2236,1466,0,4095,1466,4095,1677,1466,0],
- [2140,1240,2140,1080,1125,0,2144,1262,2144,1078,1134,0],
- [916,800,916,0,789,0,938,798,938,0,777,0],
- [3975,1129,3975,1200,1309,0,4095,1000,4095,1034,1599,0],
- [2200,1025,2200,853,1013,0,2320,1080,2345,828,1030,0],
- [972,911,972,0,0,0,974,909,974,0,0,0],
- [3975,880,3975,1020,1160,0,4095,900,4095,1000,1100,0],
- [2226,906,2226,779,881,0,2352,876,2352,787,850,0],
- [992,836,992,0,0,0,1004,824,1004,0,0,0],
- [0,0,0,0,0,0,0,0,0,0,0,1785],
- [0,0,0,0,0,0,0,0,0,0,0,1298],
- [0,0,0,0,0,0,0,0,0,0,0,743],
- [0,860,0,880,860,0,0,850,0,950,850,0],
- [0,750,0,750,750,0,0,739,0,739,739,0],
- [0,0,0,0,754,0,0,0,0,0,700,0],
- [0,0,0,0,0,0,0,0,0,0,0,4095],
- [0,0,0,0,0,0,0,0,0,0,0,2169],
- [0,0,0,0,0,0,0,0,0,0,0,1021],
- [0,0,0,0,0,1520,0,0,0,0,0,3975],
- [0,0,0,0,0,970,0,0,568,0,0,2196],
- [0,0,0,0,0,726,0,0,0,0,0,1265],
- [0,0,0,0,0,4095,0,0,0,0,0,2760],
- [0,0,0,0,0,1797,0,0,0,0,0,1773],
- [0,0,0,0,0,795,0,0,0,0,0,837],
- [0,0,0,0,0,3200,0,0,0,0,0,0],
- [0,0,0,0,0,1772,0,0,0,0,0,0],
- [0,0,0,0,0,771,0,0,0,0,0,0],
- [1960,0,1960,2560,0,4095,2100,0,2100,2800,0,0],
- [1140,0,1010,2537,0,2594,1122,0,1122,1966,0,0],
- [0,0,690,1339,0,1315,0,0,694,1442,0,0],
- [1527,3817,1527,2005,3536,0,1476,3947,1476,2069,3654,858],
- [1018,2655,1018,1367,2331,0,1018,2628,1018,1346,2315,0],
- [0,924,0,1144,1261,0,0,873,0,1090,1202,987],
- [700,1460,740,690,710,0,710,1490,750,705,740,4075],
- [0,0,0,809,1110,0,0,0,0,894,1143,2315],
- [0,0,0,0,803,0,0,0,0,0,783,1043],
- ]
- COLOR_REGIONS = {
- "sky blue": {"name": "Sky Blue", "high": 24, "mid": 25, "low": 26, "high_angle": 91.0, "low_angle": 109.0},
- "ocean blue 7": {"name": "Ocean Blue 7", "high": 21, "mid": 22, "low": 23, "high_angle": 110.0, "low_angle": 129.0},
- "ocean blue 6": {"name": "Ocean Blue 6", "high": 18, "mid": 19, "low": 20, "high_angle": 130.0, "low_angle": 149.0},
- "ocean blue 5": {"name": "Ocean Blue 5", "high": 15, "mid": 16, "low": 17, "high_angle": 150.0, "low_angle": 169.0},
- "ocean blue 4": {"name": "Ocean Blue 4", "high": 12, "mid": 13, "low": 14, "high_angle": 170.0, "low_angle": 189.0},
- "ocean blue 3": {"name": "Ocean Blue 3", "high": 9, "mid": 10, "low": 11, "high_angle": 190.0, "low_angle": 209.0},
- "ocean blue 2": {"name": "Ocean Blue 2", "high": 6, "mid": 7, "low": 8, "high_angle": 210.0, "low_angle": 229.0},
- "ocean blue 1": {"name": "Ocean Blue 1", "high": 3, "mid": 4, "low": 5, "high_angle": 230.0, "low_angle": 249.0},
- "deep ocean blue": {"name": "Deep Ocean Blue", "high": 0, "mid": 1, "low": 2, "high_angle": 250.0, "low_angle": 267.0},
- "indigo": {"name": "Indigo", "high": 48, "mid": 49, "low": 50, "high_angle": 270.0, "low_angle": 271.0},
- "purple": {"name": "Purple", "high": 51, "mid": 52, "low": 53, "high_angle": 285.0, "low_angle": 325.0},
- "red": {"name": "Red", "high": 33, "mid": 34, "low": 35, "high_angle": 325.0, "low_angle": 339.0},
- "orange": {"name": "Orange", "high": 36, "mid": 37, "low": 38, "high_angle": 340.0, "low_angle": 359.0},
- "yellow": {"name": "Yellow", "high": 39, "mid": 40, "low": 41, "high_angle": 15.0, "low_angle": 64.0},
- "green": {"name": "Green", "high": 42, "mid": 43, "low": 44, "high_angle": 65.0, "low_angle": 110.0},
- "blue": {"name": "Blue", "high": 45, "mid": 46, "low": 47, "high_angle": 90.0, "low_angle": 90.0},
- "moon red": {"name": "Moon Red", "high": 27, "mid": 28, "low": 29, "high_angle": 400.0, "low_angle": 400.0},
- "mood blue": {"name": "Mood Blue", "high": 30, "mid": 31, "low": 32, "high_angle": 401.0, "low_angle": 401.0},
- "moon blue": {"name": "Mood Blue", "high": 30, "mid": 31, "low": 32, "high_angle": 401.0, "low_angle": 401.0},
- }
- INTENSITY_MAPPING_PERCENT = [0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 7, 7, 7, 8, 8, 9, 9, 9, 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 18, 18, 18, 19, 19, 20, 20, 20, 21, 21, 22, 22, 22, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 29, 30, 30, 31, 31, 31, 32, 32, 33, 33, 33, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 38, 39, 39, 40, 40, 40, 41, 41, 42, 42, 42, 43, 43, 44, 44, 44, 45, 45, 45, 46, 46, 47, 47, 47, 48, 48, 49, 49, 49, 50, 50, 51, 51, 51, 52, 52, 53, 53, 53, 54, 54, 55, 55, 55, 56, 56, 56, 57, 57, 58, 58, 58, 59, 59, 60, 60, 60, 61, 61, 62, 62, 62, 63, 63, 64, 64, 64, 65, 65, 65, 66, 66, 67, 67, 67, 68, 68, 69, 69, 69, 70, 70, 71, 71, 71, 72, 72, 73, 73, 73, 74, 74, 75, 75, 75, 76, 76, 76, 77, 77, 78, 78, 78, 79, 79, 80, 80, 80, 81, 81, 82, 82, 82, 83, 83, 84, 84, 84, 85, 85, 85, 86, 86, 87, 87, 87, 88, 88, 89, 89, 89, 90, 90, 91, 91, 91, 92, 92, 93, 93, 93, 94, 94, 95, 95, 95, 96, 96, 96, 97, 97, 98, 98, 98, 99, 99, 100, 100]
- ALIASES = {
- "cyan": "sky blue",
- "deep blue": "deep ocean blue",
- "ocean1": "ocean blue 1",
- "ocean2": "ocean blue 2",
- "ocean3": "ocean blue 3",
- "ocean4": "ocean blue 4",
- "ocean5": "ocean blue 5",
- "ocean6": "ocean blue 6",
- "ocean7": "ocean blue 7",
- }
- def _log_error(msg):
- try:
- log.error(msg)
- except Exception:
- print(msg)
- def _normalize_name(name):
- key = str(name).strip().lower().replace("_", " ").replace("-", " ")
- return ALIASES.get(" ".join(key.split()), " ".join(key.split()))
- def _clamp(v, lo=0, hi=255):
- v = int(v)
- if v < lo:
- return lo
- if v > hi:
- return hi
- return v
- def _clamp_u16(v):
- return _clamp(v, 0, 4095)
- def _pct_to_device_byte(percent):
- target = _clamp(percent, 0, 100)
- for i, pct in enumerate(INTENSITY_MAPPING_PERCENT):
- if pct >= target:
- return i
- return 255
- def _checksum(data):
- total = 0
- for b in data:
- total = (total + int(b)) & 0xFF
- return (256 - total) & 0xFF
- def _frame(body):
- msg = [len(body) + 1] + [int(b) & 0xFF for b in body]
- chk = _checksum(msg)
- return bytes([START_BYTE] + msg + [chk, STOP_BYTE])
- def _target_hosts(host=None):
- if host is None or host == "":
- return list(LIGHT_HOSTS)
- if isinstance(host, str):
- return [host]
- return list(host)
- def _u16le(v):
- v = _clamp_u16(v)
- return [v & 0xFF, (v >> 8) & 0xFF]
- @pyscript_executor
- def _send(host, payload, timeout=TIMEOUT):
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- try:
- sock.settimeout(timeout)
- sock.connect((host, PORT))
- sock.sendall(payload)
- return True
- finally:
- try:
- sock.close()
- except Exception:
- pass
- def _send_all(payload, host=None, timeout=TIMEOUT):
- results = {}
- for one_host in _target_hosts(host):
- try:
- _send(one_host, payload, timeout=timeout)
- results[one_host] = True
- except Exception as e:
- results[one_host] = False
- _log_error("AP700 %s failed: %s" % (one_host, e))
- return results
- def _cmd_power(state):
- return _frame([0x01, 0x00, CMD_POWER, _clamp(state, 0, 1)])
- def _cmd_find_me():
- return _frame([0x01, 0x00, CMD_FIND_ME])
- def _cmd_set_all_channel_index(channels12, apply_flag=1):
- vals = list(channels12[:12])
- while len(vals) < 12:
- vals.append(1)
- body = [0x01, 0x00, CMD_SET_ALL_CHANNEL_INDEX]
- for v in vals:
- body.extend(_u16le(v))
- body.extend([0, 0, 0, 0, 0, _clamp(apply_flag, 0, 255)])
- return _frame(body)
- def _check_calibration_data650(channels12):
- out = list(channels12)
- if out[0] < 650 or out[6] < 650:
- out[0] = 1
- out[6] = 1
- if out[1] < 650 or out[7] < 650:
- out[1] = 1
- out[7] = 1
- if out[2] < 650 or out[8] < 650:
- out[2] = 1
- out[8] = 1
- if out[3] < 650 or out[9] < 650:
- out[3] = 1
- out[9] = 1
- return out
- def _get_touch_channel_index(intensity255, region_name):
- key = _normalize_name(region_name)
- region = COLOR_REGIONS[key]
- d3 = float(_clamp(intensity255, 0, 255)) / 255.0
- if d3 == 1.0:
- return list(CALIBRATION[region["high"]])
- if 0.5 < d3 < 1.0:
- high = CALIBRATION[region["high"]]
- mid = CALIBRATION[region["mid"]]
- out = []
- for i in range(12):
- out.append(int(round(((high[i] - mid[i]) / 50.0) * (d3 - 0.5) * 100.0)) + mid[i])
- return out
- if 0.0 < d3 < 0.5:
- mid = CALIBRATION[region["mid"]]
- low = CALIBRATION[region["low"]]
- out = []
- for i in range(12):
- out.append(int(round(((mid[i] - low[i]) / 50.0) * d3 * 100.0)) + low[i])
- return out
- if d3 == 0.0:
- return [1] * 12
- return list(CALIBRATION[region["mid"]])
- def _get_touch_channel_index_271_90(angle, intensity255, region_name_a, region_name_b):
- region_a = COLOR_REGIONS[_normalize_name(region_name_a)]
- touch_a = _get_touch_channel_index(intensity255, region_name_a)
- touch_b = _get_touch_channel_index(intensity255, region_name_b)
- a = float(int(angle) % 360)
- d3 = a + 360.0 if a < 15.0 else a
- span = abs(float(region_a["high_angle"]) - float(region_a["low_angle"]))
- ratio = abs(float(region_a["high_angle"]) - d3) / span if span > 0.0 else 0.0
- if ratio < 0.0:
- ratio = 0.0
- if ratio > 1.0:
- ratio = 1.0
- out = []
- for i in range(12):
- va = int((1.0 - ratio) * touch_a[i])
- vb = int(ratio * touch_b[i])
- vv = va + vb
- out.append(vv if vv >= 1 else 1)
- return out
- def _angle_to_color_pair(angle):
- a = int(angle) % 360
- if 270 <= a < 285:
- return ("Indigo", "Purple")
- if 285 <= a < 325:
- return ("Purple", "Red")
- if 325 <= a < 340:
- return ("Red", "Orange")
- if 340 <= a < 360 or 0 <= a < 15:
- return ("Orange", "Yellow")
- if 15 <= a < 65:
- return ("Yellow", "Green")
- if 65 <= a < 90:
- return ("Green", "Blue")
- if a == 90:
- return ("Blue", "Sky Blue")
- if 90 < a < 110:
- return ("Sky Blue", "Ocean Blue 7")
- if 110 <= a < 130:
- return ("Ocean Blue 7", "Ocean Blue 6")
- if 130 <= a < 150:
- return ("Ocean Blue 6", "Ocean Blue 5")
- if 150 <= a < 170:
- return ("Ocean Blue 5", "Ocean Blue 4")
- if 170 <= a < 190:
- return ("Ocean Blue 4", "Ocean Blue 3")
- if 190 <= a < 210:
- return ("Ocean Blue 3", "Ocean Blue 2")
- if 210 <= a < 230:
- return ("Ocean Blue 2", "Ocean Blue 1")
- if 230 <= a < 250:
- return ("Ocean Blue 1", "Deep Ocean Blue")
- if 250 <= a < 270:
- return ("Deep Ocean Blue", "Indigo")
- raise ValueError("unsupported angle %s" % angle)
- def _channels_for_angle(angle, intensity=100):
- angle = int(angle) % 360
- intensity255 = _pct_to_device_byte(intensity)
- color_a, color_b = _angle_to_color_pair(angle)
- if intensity255 <= 0:
- return [1] * 12
- if angle <= 90 or angle > 269:
- channels = _get_touch_channel_index_271_90(angle, intensity255, color_a, color_b)
- else:
- channels = _get_touch_channel_index(intensity255, color_a)
- return _check_calibration_data650(channels)
- def _row_scaled(row_index, intensity=100):
- src = CALIBRATION[int(row_index)]
- intensity255 = _pct_to_device_byte(intensity)
- if intensity255 <= 0:
- return [1] * 12
- factor = intensity255 / 255.0
- out = []
- for v in src:
- vv = int(round(v * factor))
- out.append(_clamp_u16(vv if vv > 0 else 1))
- return out
- @service
- def ap700_on(host=None, **kwargs):
- return _send_all(_cmd_power(1), host=host)
- @service
- def ap700_off(host=None, **kwargs):
- return _send_all(_cmd_power(0), host=host)
- @service
- def ap700_power(state=1, host=None, **kwargs):
- return _send_all(_cmd_power(state), host=host)
- @service
- def ap700_find_me(host=None, **kwargs):
- return _send_all(_cmd_find_me(), host=host)
- @service
- def ap700_send_hex(hex_payload="", host=None, **kwargs):
- s = str(hex_payload).strip().replace(" ", "").replace("0x", "")
- if not s:
- raise ValueError("hex_payload is required")
- if len(s) % 2 != 0:
- raise ValueError("hex_payload must contain an even number of hex digits")
- return _send_all(bytes.fromhex(s), host=host)
- @service
- def ap700_set_channels12(channels=None, apply_flag=1, host=None, **kwargs):
- vals = list(channels) if not isinstance(channels, str) else [int(x.strip(), 0) for x in channels.split(",") if x.strip()]
- if len(vals) != 12:
- raise ValueError("channels must contain exactly 12 values")
- device_byte = _pct_to_device_byte(apply_flag)
- return _send_all(_cmd_set_all_channel_index(vals, apply_flag=device_byte), host=host)
- @service
- def ap700_set_calibration_row(row=0, intensity=100, host=None, **kwargs):
- row = int(row)
- if row < 0 or row >= len(CALIBRATION):
- raise ValueError("row must be between 0 and %s" % (len(CALIBRATION) - 1))
- device_byte = _pct_to_device_byte(intensity)
- return _send_all(_cmd_set_all_channel_index(_row_scaled(row, intensity=intensity), apply_flag=device_byte), host=host)
- @service
- def ap700_set_angle(angle=200, intensity=100, host=None, **kwargs):
- device_byte = _pct_to_device_byte(intensity)
- return _send_all(_cmd_set_all_channel_index(_channels_for_angle(angle, intensity=intensity), apply_flag=device_byte), host=host)
- @service
- def ap700_set_color_name(color_name="Ocean Blue 3", intensity=100, host=None, **kwargs):
- key = _normalize_name(color_name)
- if key not in COLOR_REGIONS:
- raise ValueError("Unknown color_name: %s" % color_name)
- region = COLOR_REGIONS[key]
- angle = int(round((region["high_angle"] + region["low_angle"]) / 2.0)) % 360
- if key == "blue":
- angle = 90
- if key == "indigo":
- angle = 270
- if key == "moon red":
- return _send_all(_cmd_set_all_channel_index(_row_scaled(28, intensity=intensity), apply_flag=1), host=host)
- if key in ("mood blue", "moon blue"):
- return _send_all(_cmd_set_all_channel_index(_row_scaled(31, intensity=intensity), apply_flag=1), host=host)
- return ap700_set_angle(angle=angle, intensity=intensity, host=host)
- @service
- def ap700_color_names(**kwargs):
- names = []
- seen = set()
- for key in sorted(COLOR_REGIONS.keys()):
- display = COLOR_REGIONS[key]["name"]
- if display not in seen:
- names.append(display)
- seen.add(display)
- return names
Advertisement
Add Comment
Please, Sign In to add comment