Guest User

Untitled

a guest
Apr 13th, 2026
60
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.43 KB | None | 0 0
  1. """
  2. AP700 write-only PyScript using older APK default calibration rows.
  3.  
  4. What this version does:
  5. - keeps the known-good AP700 TCP framing/checksum
  6. - uses SET_ALL_CHANNEL_INDEX only
  7. - uses the older APK's built-in default calibration rows (0..53) for named colors
  8. - computes channels from angle using the app-style color-region table
  9. - uses user-facing intensity 0..100
  10. - does NO live reads
  11.  
  12. Install:
  13. Save as /config/pyscript/ap700_default_cal.py
  14. Reload PyScript
  15.  
  16. Services:
  17. pyscript.ap700_on
  18. pyscript.ap700_off
  19. pyscript.ap700_power
  20. pyscript.ap700_find_me
  21. pyscript.ap700_send_hex
  22. pyscript.ap700_set_channels12
  23. pyscript.ap700_set_calibration_row
  24. pyscript.ap700_set_angle
  25. pyscript.ap700_set_color_name
  26. pyscript.ap700_color_names
  27. """
  28.  
  29. import socket
  30.  
  31. LIGHT_HOSTS = ["10.50.52.2", "10.50.52.3"]
  32. PORT = 8899
  33. TIMEOUT = 0.7
  34.  
  35. START_BYTE = 0xA9
  36. STOP_BYTE = 0x5C
  37.  
  38. CMD_SET_ALL_CHANNEL_INDEX = 0x01
  39. CMD_POWER = 0x27
  40. CMD_FIND_ME = 0x76
  41.  
  42. # Older APK built-in default calibration rows 0..53.
  43. CALIBRATION = [
  44. [720,3028,720,4095,4095,0,670,3632,670,3913,3913,0],
  45. [0,2318,0,2060,2060,0,0,2250,0,2000,2000,0],
  46. [0,807,0,0,1421,0,0,800,0,0,1400,0],
  47. [1100,2813,1100,4095,4095,0,1084,2746,1084,4034,4034,0],
  48. [821,2144,821,1870,2082,0,847,2118,847,1857,2056,0],
  49. [0,1255,731,794,786,0,0,1236,721,772,773,0],
  50. [1660,1704,1660,4095,4095,0,1626,1743,1626,4095,4095,0],
  51. [1071,1953,1164,1829,1878,0,1071,1953,1164,1829,1878,0],
  52. [724,1212,724,759,805,0,0,1178,875,751,746,0],
  53. [2223,1000,2223,3568,3508,0,2200,1635,2200,4095,2734,0],
  54. [1405,1684,1405,1720,1684,0,1405,1684,1405,1720,1684,0],
  55. [788,986,788,0,986,0,786,961,786,0,961,0],
  56. [2684,703,2684,3486,2955,0,2695,985,2695,3181,2734,0],
  57. [1676,1676,1676,1390,1502,0,1654,1698,1654,1401,1524,0],
  58. [832,913,832,0,913,0,826,903,826,0,898,0],
  59. [3161,770,3161,4054,1833,0,3333,1915,3333,2069,1967,0],
  60. [1918,1429,1918,1259,1314,0,1922,1451,1922,1257,1323,0],
  61. [885,857,885,0,857,0,884,852,884,0,842,0],
  62. [4015,1125,4015,2236,1466,0,4095,1466,4095,1677,1466,0],
  63. [2140,1240,2140,1080,1125,0,2144,1262,2144,1078,1134,0],
  64. [916,800,916,0,789,0,938,798,938,0,777,0],
  65. [3975,1129,3975,1200,1309,0,4095,1000,4095,1034,1599,0],
  66. [2200,1025,2200,853,1013,0,2320,1080,2345,828,1030,0],
  67. [972,911,972,0,0,0,974,909,974,0,0,0],
  68. [3975,880,3975,1020,1160,0,4095,900,4095,1000,1100,0],
  69. [2226,906,2226,779,881,0,2352,876,2352,787,850,0],
  70. [992,836,992,0,0,0,1004,824,1004,0,0,0],
  71. [0,0,0,0,0,0,0,0,0,0,0,1785],
  72. [0,0,0,0,0,0,0,0,0,0,0,1298],
  73. [0,0,0,0,0,0,0,0,0,0,0,743],
  74. [0,860,0,880,860,0,0,850,0,950,850,0],
  75. [0,750,0,750,750,0,0,739,0,739,739,0],
  76. [0,0,0,0,754,0,0,0,0,0,700,0],
  77. [0,0,0,0,0,0,0,0,0,0,0,4095],
  78. [0,0,0,0,0,0,0,0,0,0,0,2169],
  79. [0,0,0,0,0,0,0,0,0,0,0,1021],
  80. [0,0,0,0,0,1520,0,0,0,0,0,3975],
  81. [0,0,0,0,0,970,0,0,568,0,0,2196],
  82. [0,0,0,0,0,726,0,0,0,0,0,1265],
  83. [0,0,0,0,0,4095,0,0,0,0,0,2760],
  84. [0,0,0,0,0,1797,0,0,0,0,0,1773],
  85. [0,0,0,0,0,795,0,0,0,0,0,837],
  86. [0,0,0,0,0,3200,0,0,0,0,0,0],
  87. [0,0,0,0,0,1772,0,0,0,0,0,0],
  88. [0,0,0,0,0,771,0,0,0,0,0,0],
  89. [1960,0,1960,2560,0,4095,2100,0,2100,2800,0,0],
  90. [1140,0,1010,2537,0,2594,1122,0,1122,1966,0,0],
  91. [0,0,690,1339,0,1315,0,0,694,1442,0,0],
  92. [1527,3817,1527,2005,3536,0,1476,3947,1476,2069,3654,858],
  93. [1018,2655,1018,1367,2331,0,1018,2628,1018,1346,2315,0],
  94. [0,924,0,1144,1261,0,0,873,0,1090,1202,987],
  95. [700,1460,740,690,710,0,710,1490,750,705,740,4075],
  96. [0,0,0,809,1110,0,0,0,0,894,1143,2315],
  97. [0,0,0,0,803,0,0,0,0,0,783,1043],
  98. ]
  99.  
  100. COLOR_REGIONS = {
  101. "sky blue": {"name": "Sky Blue", "high": 24, "mid": 25, "low": 26, "high_angle": 91.0, "low_angle": 109.0},
  102. "ocean blue 7": {"name": "Ocean Blue 7", "high": 21, "mid": 22, "low": 23, "high_angle": 110.0, "low_angle": 129.0},
  103. "ocean blue 6": {"name": "Ocean Blue 6", "high": 18, "mid": 19, "low": 20, "high_angle": 130.0, "low_angle": 149.0},
  104. "ocean blue 5": {"name": "Ocean Blue 5", "high": 15, "mid": 16, "low": 17, "high_angle": 150.0, "low_angle": 169.0},
  105. "ocean blue 4": {"name": "Ocean Blue 4", "high": 12, "mid": 13, "low": 14, "high_angle": 170.0, "low_angle": 189.0},
  106. "ocean blue 3": {"name": "Ocean Blue 3", "high": 9, "mid": 10, "low": 11, "high_angle": 190.0, "low_angle": 209.0},
  107. "ocean blue 2": {"name": "Ocean Blue 2", "high": 6, "mid": 7, "low": 8, "high_angle": 210.0, "low_angle": 229.0},
  108. "ocean blue 1": {"name": "Ocean Blue 1", "high": 3, "mid": 4, "low": 5, "high_angle": 230.0, "low_angle": 249.0},
  109. "deep ocean blue": {"name": "Deep Ocean Blue", "high": 0, "mid": 1, "low": 2, "high_angle": 250.0, "low_angle": 267.0},
  110. "indigo": {"name": "Indigo", "high": 48, "mid": 49, "low": 50, "high_angle": 270.0, "low_angle": 271.0},
  111. "purple": {"name": "Purple", "high": 51, "mid": 52, "low": 53, "high_angle": 285.0, "low_angle": 325.0},
  112. "red": {"name": "Red", "high": 33, "mid": 34, "low": 35, "high_angle": 325.0, "low_angle": 339.0},
  113. "orange": {"name": "Orange", "high": 36, "mid": 37, "low": 38, "high_angle": 340.0, "low_angle": 359.0},
  114. "yellow": {"name": "Yellow", "high": 39, "mid": 40, "low": 41, "high_angle": 15.0, "low_angle": 64.0},
  115. "green": {"name": "Green", "high": 42, "mid": 43, "low": 44, "high_angle": 65.0, "low_angle": 110.0},
  116. "blue": {"name": "Blue", "high": 45, "mid": 46, "low": 47, "high_angle": 90.0, "low_angle": 90.0},
  117. "moon red": {"name": "Moon Red", "high": 27, "mid": 28, "low": 29, "high_angle": 400.0, "low_angle": 400.0},
  118. "mood blue": {"name": "Mood Blue", "high": 30, "mid": 31, "low": 32, "high_angle": 401.0, "low_angle": 401.0},
  119. "moon blue": {"name": "Mood Blue", "high": 30, "mid": 31, "low": 32, "high_angle": 401.0, "low_angle": 401.0},
  120. }
  121.  
  122.  
  123. 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]
  124.  
  125. ALIASES = {
  126. "cyan": "sky blue",
  127. "deep blue": "deep ocean blue",
  128. "ocean1": "ocean blue 1",
  129. "ocean2": "ocean blue 2",
  130. "ocean3": "ocean blue 3",
  131. "ocean4": "ocean blue 4",
  132. "ocean5": "ocean blue 5",
  133. "ocean6": "ocean blue 6",
  134. "ocean7": "ocean blue 7",
  135. }
  136.  
  137. def _log_error(msg):
  138. try:
  139. log.error(msg)
  140. except Exception:
  141. print(msg)
  142.  
  143. def _normalize_name(name):
  144. key = str(name).strip().lower().replace("_", " ").replace("-", " ")
  145. return ALIASES.get(" ".join(key.split()), " ".join(key.split()))
  146.  
  147. def _clamp(v, lo=0, hi=255):
  148. v = int(v)
  149. if v < lo:
  150. return lo
  151. if v > hi:
  152. return hi
  153. return v
  154.  
  155. def _clamp_u16(v):
  156. return _clamp(v, 0, 4095)
  157.  
  158. def _pct_to_device_byte(percent):
  159. target = _clamp(percent, 0, 100)
  160. for i, pct in enumerate(INTENSITY_MAPPING_PERCENT):
  161. if pct >= target:
  162. return i
  163. return 255
  164.  
  165. def _checksum(data):
  166. total = 0
  167. for b in data:
  168. total = (total + int(b)) & 0xFF
  169. return (256 - total) & 0xFF
  170.  
  171. def _frame(body):
  172. msg = [len(body) + 1] + [int(b) & 0xFF for b in body]
  173. chk = _checksum(msg)
  174. return bytes([START_BYTE] + msg + [chk, STOP_BYTE])
  175.  
  176. def _target_hosts(host=None):
  177. if host is None or host == "":
  178. return list(LIGHT_HOSTS)
  179. if isinstance(host, str):
  180. return [host]
  181. return list(host)
  182.  
  183. def _u16le(v):
  184. v = _clamp_u16(v)
  185. return [v & 0xFF, (v >> 8) & 0xFF]
  186.  
  187. @pyscript_executor
  188. def _send(host, payload, timeout=TIMEOUT):
  189. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  190. try:
  191. sock.settimeout(timeout)
  192. sock.connect((host, PORT))
  193. sock.sendall(payload)
  194. return True
  195. finally:
  196. try:
  197. sock.close()
  198. except Exception:
  199. pass
  200.  
  201. def _send_all(payload, host=None, timeout=TIMEOUT):
  202. results = {}
  203. for one_host in _target_hosts(host):
  204. try:
  205. _send(one_host, payload, timeout=timeout)
  206. results[one_host] = True
  207. except Exception as e:
  208. results[one_host] = False
  209. _log_error("AP700 %s failed: %s" % (one_host, e))
  210. return results
  211.  
  212. def _cmd_power(state):
  213. return _frame([0x01, 0x00, CMD_POWER, _clamp(state, 0, 1)])
  214.  
  215. def _cmd_find_me():
  216. return _frame([0x01, 0x00, CMD_FIND_ME])
  217.  
  218. def _cmd_set_all_channel_index(channels12, apply_flag=1):
  219. vals = list(channels12[:12])
  220. while len(vals) < 12:
  221. vals.append(1)
  222. body = [0x01, 0x00, CMD_SET_ALL_CHANNEL_INDEX]
  223. for v in vals:
  224. body.extend(_u16le(v))
  225. body.extend([0, 0, 0, 0, 0, _clamp(apply_flag, 0, 255)])
  226. return _frame(body)
  227.  
  228. def _check_calibration_data650(channels12):
  229. out = list(channels12)
  230. if out[0] < 650 or out[6] < 650:
  231. out[0] = 1
  232. out[6] = 1
  233. if out[1] < 650 or out[7] < 650:
  234. out[1] = 1
  235. out[7] = 1
  236. if out[2] < 650 or out[8] < 650:
  237. out[2] = 1
  238. out[8] = 1
  239. if out[3] < 650 or out[9] < 650:
  240. out[3] = 1
  241. out[9] = 1
  242. return out
  243.  
  244. def _get_touch_channel_index(intensity255, region_name):
  245. key = _normalize_name(region_name)
  246. region = COLOR_REGIONS[key]
  247. d3 = float(_clamp(intensity255, 0, 255)) / 255.0
  248. if d3 == 1.0:
  249. return list(CALIBRATION[region["high"]])
  250. if 0.5 < d3 < 1.0:
  251. high = CALIBRATION[region["high"]]
  252. mid = CALIBRATION[region["mid"]]
  253. out = []
  254. for i in range(12):
  255. out.append(int(round(((high[i] - mid[i]) / 50.0) * (d3 - 0.5) * 100.0)) + mid[i])
  256. return out
  257. if 0.0 < d3 < 0.5:
  258. mid = CALIBRATION[region["mid"]]
  259. low = CALIBRATION[region["low"]]
  260. out = []
  261. for i in range(12):
  262. out.append(int(round(((mid[i] - low[i]) / 50.0) * d3 * 100.0)) + low[i])
  263. return out
  264. if d3 == 0.0:
  265. return [1] * 12
  266. return list(CALIBRATION[region["mid"]])
  267.  
  268. def _get_touch_channel_index_271_90(angle, intensity255, region_name_a, region_name_b):
  269. region_a = COLOR_REGIONS[_normalize_name(region_name_a)]
  270. touch_a = _get_touch_channel_index(intensity255, region_name_a)
  271. touch_b = _get_touch_channel_index(intensity255, region_name_b)
  272. a = float(int(angle) % 360)
  273. d3 = a + 360.0 if a < 15.0 else a
  274. span = abs(float(region_a["high_angle"]) - float(region_a["low_angle"]))
  275. ratio = abs(float(region_a["high_angle"]) - d3) / span if span > 0.0 else 0.0
  276. if ratio < 0.0:
  277. ratio = 0.0
  278. if ratio > 1.0:
  279. ratio = 1.0
  280. out = []
  281. for i in range(12):
  282. va = int((1.0 - ratio) * touch_a[i])
  283. vb = int(ratio * touch_b[i])
  284. vv = va + vb
  285. out.append(vv if vv >= 1 else 1)
  286. return out
  287.  
  288. def _angle_to_color_pair(angle):
  289. a = int(angle) % 360
  290. if 270 <= a < 285:
  291. return ("Indigo", "Purple")
  292. if 285 <= a < 325:
  293. return ("Purple", "Red")
  294. if 325 <= a < 340:
  295. return ("Red", "Orange")
  296. if 340 <= a < 360 or 0 <= a < 15:
  297. return ("Orange", "Yellow")
  298. if 15 <= a < 65:
  299. return ("Yellow", "Green")
  300. if 65 <= a < 90:
  301. return ("Green", "Blue")
  302. if a == 90:
  303. return ("Blue", "Sky Blue")
  304. if 90 < a < 110:
  305. return ("Sky Blue", "Ocean Blue 7")
  306. if 110 <= a < 130:
  307. return ("Ocean Blue 7", "Ocean Blue 6")
  308. if 130 <= a < 150:
  309. return ("Ocean Blue 6", "Ocean Blue 5")
  310. if 150 <= a < 170:
  311. return ("Ocean Blue 5", "Ocean Blue 4")
  312. if 170 <= a < 190:
  313. return ("Ocean Blue 4", "Ocean Blue 3")
  314. if 190 <= a < 210:
  315. return ("Ocean Blue 3", "Ocean Blue 2")
  316. if 210 <= a < 230:
  317. return ("Ocean Blue 2", "Ocean Blue 1")
  318. if 230 <= a < 250:
  319. return ("Ocean Blue 1", "Deep Ocean Blue")
  320. if 250 <= a < 270:
  321. return ("Deep Ocean Blue", "Indigo")
  322. raise ValueError("unsupported angle %s" % angle)
  323.  
  324. def _channels_for_angle(angle, intensity=100):
  325. angle = int(angle) % 360
  326. intensity255 = _pct_to_device_byte(intensity)
  327. color_a, color_b = _angle_to_color_pair(angle)
  328. if intensity255 <= 0:
  329. return [1] * 12
  330. if angle <= 90 or angle > 269:
  331. channels = _get_touch_channel_index_271_90(angle, intensity255, color_a, color_b)
  332. else:
  333. channels = _get_touch_channel_index(intensity255, color_a)
  334. return _check_calibration_data650(channels)
  335.  
  336. def _row_scaled(row_index, intensity=100):
  337. src = CALIBRATION[int(row_index)]
  338. intensity255 = _pct_to_device_byte(intensity)
  339. if intensity255 <= 0:
  340. return [1] * 12
  341. factor = intensity255 / 255.0
  342. out = []
  343. for v in src:
  344. vv = int(round(v * factor))
  345. out.append(_clamp_u16(vv if vv > 0 else 1))
  346. return out
  347.  
  348. @service
  349. def ap700_on(host=None, **kwargs):
  350. return _send_all(_cmd_power(1), host=host)
  351.  
  352. @service
  353. def ap700_off(host=None, **kwargs):
  354. return _send_all(_cmd_power(0), host=host)
  355.  
  356. @service
  357. def ap700_power(state=1, host=None, **kwargs):
  358. return _send_all(_cmd_power(state), host=host)
  359.  
  360. @service
  361. def ap700_find_me(host=None, **kwargs):
  362. return _send_all(_cmd_find_me(), host=host)
  363.  
  364. @service
  365. def ap700_send_hex(hex_payload="", host=None, **kwargs):
  366. s = str(hex_payload).strip().replace(" ", "").replace("0x", "")
  367. if not s:
  368. raise ValueError("hex_payload is required")
  369. if len(s) % 2 != 0:
  370. raise ValueError("hex_payload must contain an even number of hex digits")
  371. return _send_all(bytes.fromhex(s), host=host)
  372.  
  373. @service
  374. def ap700_set_channels12(channels=None, apply_flag=1, host=None, **kwargs):
  375. vals = list(channels) if not isinstance(channels, str) else [int(x.strip(), 0) for x in channels.split(",") if x.strip()]
  376. if len(vals) != 12:
  377. raise ValueError("channels must contain exactly 12 values")
  378. device_byte = _pct_to_device_byte(apply_flag)
  379. return _send_all(_cmd_set_all_channel_index(vals, apply_flag=device_byte), host=host)
  380.  
  381. @service
  382. def ap700_set_calibration_row(row=0, intensity=100, host=None, **kwargs):
  383. row = int(row)
  384. if row < 0 or row >= len(CALIBRATION):
  385. raise ValueError("row must be between 0 and %s" % (len(CALIBRATION) - 1))
  386. device_byte = _pct_to_device_byte(intensity)
  387. return _send_all(_cmd_set_all_channel_index(_row_scaled(row, intensity=intensity), apply_flag=device_byte), host=host)
  388.  
  389. @service
  390. def ap700_set_angle(angle=200, intensity=100, host=None, **kwargs):
  391. device_byte = _pct_to_device_byte(intensity)
  392. return _send_all(_cmd_set_all_channel_index(_channels_for_angle(angle, intensity=intensity), apply_flag=device_byte), host=host)
  393.  
  394. @service
  395. def ap700_set_color_name(color_name="Ocean Blue 3", intensity=100, host=None, **kwargs):
  396. key = _normalize_name(color_name)
  397. if key not in COLOR_REGIONS:
  398. raise ValueError("Unknown color_name: %s" % color_name)
  399. region = COLOR_REGIONS[key]
  400. angle = int(round((region["high_angle"] + region["low_angle"]) / 2.0)) % 360
  401. if key == "blue":
  402. angle = 90
  403. if key == "indigo":
  404. angle = 270
  405. if key == "moon red":
  406. return _send_all(_cmd_set_all_channel_index(_row_scaled(28, intensity=intensity), apply_flag=1), host=host)
  407. if key in ("mood blue", "moon blue"):
  408. return _send_all(_cmd_set_all_channel_index(_row_scaled(31, intensity=intensity), apply_flag=1), host=host)
  409. return ap700_set_angle(angle=angle, intensity=intensity, host=host)
  410.  
  411. @service
  412. def ap700_color_names(**kwargs):
  413. names = []
  414. seen = set()
  415. for key in sorted(COLOR_REGIONS.keys()):
  416. display = COLOR_REGIONS[key]["name"]
  417. if display not in seen:
  418. names.append(display)
  419. seen.add(display)
  420. return names
  421.  
  422.  
Advertisement
Add Comment
Please, Sign In to add comment