vsslctrl 0.1.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vsslctrl/api_alpha.py ADDED
@@ -0,0 +1,1040 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ import json
4
+ import time
5
+ import struct
6
+ import logging
7
+
8
+ from .api_base import APIBase
9
+ from .transport import ZoneTransport
10
+ from .settings import EQSettings
11
+
12
+ from .utils import hex_to_int, clamp_volume
13
+ from .decorators import logging_helpers
14
+ from .data_structure import (
15
+ ZoneStatusExtKeys,
16
+ ZoneEQStatusExtKeys,
17
+ ZoneRouterStatusExtKeys,
18
+ DeviceStatusExtKeys,
19
+ )
20
+
21
+
22
+ @logging_helpers()
23
+ class APIAlpha(APIBase):
24
+ TCP_PORT = 50002
25
+ HEADER_LENGTH = 3
26
+ JSON_HEADER_LENGTH = HEADER_LENGTH + 1 # action is 1 byte
27
+
28
+ def __init__(self, vssl_host: "core.Vssl", zone: "zone.Zone"):
29
+ super().__init__(host=zone.host, port=self.TCP_PORT)
30
+
31
+ self._log_prefix = f"Zone {zone.id}: Alpha API:"
32
+
33
+ self.vssl = vssl_host
34
+ self.zone = zone
35
+
36
+ #
37
+ # Send keep alive
38
+ #
39
+ def _send_keepalive(self):
40
+ self.request_action_17()
41
+
42
+ #
43
+ #
44
+ #
45
+ # Requests
46
+ #
47
+ #
48
+ #
49
+ def _add_zone_id_to_request(self, command: bytearray, index: int = 3):
50
+ command[index] = self.zone.id
51
+ return command
52
+
53
+ #
54
+ # 17 [23]
55
+ # Keep Alive
56
+ #
57
+ def request_action_17(self):
58
+ self._log_debug("Requesting keep alive")
59
+ self.send(bytearray([16, 23, 1, 7]))
60
+
61
+ #
62
+ # 00 [0] - 00 [0]
63
+ # Status Bus
64
+ #
65
+ def request_action_00_00(self):
66
+ self._log_debug("Requesting device status")
67
+ self.send(bytearray([16, 0, 1, 0])) # HEX: 10000100
68
+
69
+ #
70
+ # 00 [0] - 08 [8]
71
+ # Status General
72
+ #
73
+ ZONE_STATUS = bytearray([16, 0, 1, 8])
74
+
75
+ def request_action_00_08(self):
76
+ self._log_debug("Requesting zone status")
77
+ self.send(self.ZONE_STATUS) # HEX: 10000108
78
+
79
+ #
80
+ # 00 [0] - 09 [9]
81
+ # Status EQ
82
+ #
83
+ def request_action_00_09(self):
84
+ self._log_debug("Requesting EQ status")
85
+ self.send(bytearray([16, 0, 1, 9])) # HEX: 10000109
86
+
87
+ #
88
+ # 00 [0] - 0A [10]
89
+ # Status Output
90
+ #
91
+ def request_action_00_0A(self):
92
+ self._log_debug("Requesting output status")
93
+ self.send(bytearray([16, 0, 1, 10])) # HEX: 1000010A
94
+
95
+ #
96
+ # 00 [0] - 0B [11]
97
+ # Status BT (Bluetooth)
98
+ #
99
+ def request_action_00_0B(self):
100
+ self._log_debug("Requesting status bluetooth")
101
+ self.send(bytearray([16, 0, 1, 11])) # HEX: 1000010B
102
+
103
+ #
104
+ # 03 [3]
105
+ # Input Source Set
106
+ #
107
+ def request_action_03(self, src: int):
108
+ self._log_debug(f"Requesting to change input source to {src}")
109
+ command = self._add_zone_id_to_request(bytearray([16, 3, 2, 0, src]))
110
+ self.send(command)
111
+
112
+ #
113
+ # 04 [4]
114
+ # Input Source Get
115
+ #
116
+ def request_action_04(self):
117
+ self._log_debug("Requesting input source")
118
+ command = self._add_zone_id_to_request(bytearray([16, 4, 1, 0]))
119
+ self.send(command)
120
+
121
+ #
122
+ # 05 [5]
123
+ # Set Volume
124
+ #
125
+ def request_action_05(self, vol: int):
126
+ vol = clamp_volume(vol)
127
+ self._log_debug(f"Requesting to set volume level: {vol}")
128
+ command = self._add_zone_id_to_request(bytearray([16, 5, 3, 0, vol, 3]))
129
+ self.send(command)
130
+
131
+ #
132
+ # 05 [5]
133
+ # Volume Raise
134
+ #
135
+ def request_action_05_raise(self):
136
+ self._log_debug("Requesting raise volume")
137
+ command = self._add_zone_id_to_request(bytearray([16, 5, 3, 0, 255, 3]))
138
+ self.send(command)
139
+
140
+ #
141
+ # 05 [5]
142
+ # Volume Lower
143
+ #
144
+ def request_action_05_lower(self):
145
+ self._log_debug("Requesting lower volume")
146
+ command = self._add_zone_id_to_request(bytearray([16, 5, 3, 0, 254, 3]))
147
+ self.send(command)
148
+
149
+ #
150
+ # 05 [5]
151
+ # Set Default On Volume
152
+ #
153
+ def request_action_05_08(self, vol: int):
154
+ vol = clamp_volume(vol)
155
+ self._log_debug(f"Requesting to set default on volume level: {vol}")
156
+ command = self._add_zone_id_to_request(bytearray([16, 5, 3, 0, vol, 8]))
157
+ self.send(command)
158
+
159
+ #
160
+ # 05 [5]
161
+ # Set Analog Input Fixed Gain
162
+ #
163
+ def request_action_05_00(self, gain: int):
164
+ gain = clamp_volume(gain)
165
+ self._log_debug(f"Requesting to set fix analog input gain: {gain}")
166
+ command = self._add_zone_id_to_request(bytearray([16, 5, 3, 0, gain, 0]))
167
+ self.send(command)
168
+
169
+ #
170
+ # 05 [5]
171
+ # Set Max Left Volume
172
+ #
173
+ def request_action_05_01(self, vol: int):
174
+ vol = clamp_volume(vol)
175
+ self._log_debug(f"Requesting to set left max volume: {vol}")
176
+ command = self._add_zone_id_to_request(bytearray([16, 5, 3, 0, vol, 1]))
177
+ self.send(command)
178
+
179
+ #
180
+ # 05 [5]
181
+ # Set Max Right Volume
182
+ #
183
+ def request_action_05_02(self, vol: int):
184
+ vol = clamp_volume(vol)
185
+ self._log_debug(f"Requesting to set right max volume: {vol}")
186
+ command = self._add_zone_id_to_request(bytearray([16, 5, 3, 0, vol, 2]))
187
+ self.send(command)
188
+
189
+ #
190
+ # 07 [7]
191
+ # Status Transport State
192
+ #
193
+ def request_action_07(self):
194
+ self._log_debug("Requesting status transport state")
195
+ self.send(bytearray([16, 7, 1, 0]))
196
+
197
+ #
198
+ # 0D [13]
199
+ # EQ
200
+ #
201
+ def request_action_0D(self, freq: "EQSettings.Freqs", value: int = 0):
202
+ clamped = max(90, min(value, 110))
203
+ self._log_debug(
204
+ f"Requesting to set EQ: {freq.name[1:]} ({freq.value}) to {clamped}"
205
+ )
206
+ command = self._add_zone_id_to_request(
207
+ bytearray([16, 13, 3, 0, freq.value, clamped])
208
+ )
209
+ self.send(command)
210
+
211
+ #
212
+ # 0F [15]
213
+ # Output Set Mono
214
+ #
215
+ def request_action_mono_set(self, state: int):
216
+ self._log_debug(f"Requesting to set output to mono: {state}")
217
+ command = self._add_zone_id_to_request(
218
+ bytearray([16, 15, 2, 0, int(not not state)])
219
+ )
220
+ self.send(command)
221
+
222
+ #
223
+ # 11 [17]
224
+ # Mute
225
+ #
226
+ def request_action_11(self, state: int):
227
+ self._log_debug(f"Requesting to mute volume: {state}")
228
+ command = self._add_zone_id_to_request(
229
+ bytearray([16, 17, 2, 0, int(not not state)])
230
+ )
231
+ self.send(command)
232
+
233
+ #
234
+ # 12 [18]
235
+ # Status Mute
236
+ #
237
+ def request_action_12(self):
238
+ self._log_debug(f"Requesting status mute")
239
+ command = self._add_zone_id_to_request(bytearray([16, 18, 1, 0]))
240
+ self.send(command)
241
+
242
+ #
243
+ # 25 [37]
244
+ # Enable or Disable Zone
245
+ #
246
+ # 1 = Disable
247
+ # 0 = Enable
248
+ #
249
+ def request_action_25(self, disable: bool = True):
250
+ self._log_debug(f"Requesting disable zone: {disable}")
251
+ command = self._add_zone_id_to_request(
252
+ bytearray([16, 37, 2, 0, int(not not disable)])
253
+ )
254
+ self.send(command)
255
+
256
+ #
257
+ # 15 [21]
258
+ # Set Analog Input Name / Rename Analog Input
259
+ #
260
+ def request_action_15(self, name: str):
261
+ name = name.strip()
262
+ self._log_debug(f"Requesting to change analog input name: {name}")
263
+ command = bytearray([16, 21])
264
+ command.extend(struct.pack(">B", len(name) + 1))
265
+ command.extend([0]) # zone id placeholder
266
+ command = self._add_zone_id_to_request(command)
267
+ command.extend(name.encode("utf-8"))
268
+ self.send(command)
269
+
270
+ #
271
+ # 15 [21]
272
+ # Set Optical Input Name / Rename Optical Input
273
+ #
274
+ def request_action_15_12(self, name: str):
275
+ name = name.strip()
276
+ self._log_debug(f"Requesting to change optical input name: {name}")
277
+ command = bytearray([16, 21])
278
+ command.extend(struct.pack(">B", len(name) + 1))
279
+ command.extend([12])
280
+ command.extend(name.encode("utf-8"))
281
+ self.send(command)
282
+
283
+ #
284
+ # 18 [24]
285
+ # Set Device Name / Rename Device
286
+ #
287
+ def request_action_18(self, name: str):
288
+ name = name.strip()
289
+ self._log_debug(f"Requesting to change device name: {name}")
290
+ command = bytearray([16, 24])
291
+ command.extend(struct.pack(">B", len(name) + 1))
292
+ command.extend([7])
293
+ command.extend(name.encode("utf-8"))
294
+ self.send(command)
295
+
296
+ #
297
+ # 19 [25]
298
+ # Get Device name
299
+ #
300
+ def request_action_19(self):
301
+ self._log_debug(f"Requesting device name")
302
+ command = self._add_zone_id_to_request(bytearray([16, 25, 1, 0]))
303
+ self.send(command)
304
+
305
+ #
306
+ # 1D [29]
307
+ # Analog Output Set Src
308
+ #
309
+ def request_action_1D(self, src: int):
310
+ self._log_debug(f"Requesting to change analog ouput source to {src}")
311
+ command = self._add_zone_id_to_request(bytearray([16, 29, 2, 0, src]))
312
+ self.send(command)
313
+
314
+ def request_action_1D_router(self, ao_id: int, src: int):
315
+ self._log_debug(f"Requesting to change analog ouput {ao_id} source to {src}")
316
+ self.send(bytearray([16, 29, 2, ao_id, src]))
317
+
318
+ #
319
+ # 49 [73]
320
+ # Analog Output Fix Output Vol
321
+ #
322
+ def request_action_49(self, fix: bool):
323
+ self._log_debug(f"Requesting to fix analog ouput volume {fix}")
324
+ command = self._add_zone_id_to_request(
325
+ bytearray([16, 73, 2, 0, int(not not fix)])
326
+ )
327
+ self.send(command)
328
+
329
+ def request_action_49_router(self, ao_id: int, fix: bool):
330
+ self._log_debug(f"Requesting to fix analog ouput {ao_id} volume {fix}")
331
+ self.send(bytearray([16, 73, 2, ao_id, int(not not fix)]))
332
+
333
+ #
334
+ # 3D [61]
335
+ # Transport State
336
+ #
337
+ def request_action_3D(self, state: ZoneTransport.States):
338
+ if state == ZoneTransport.States.STOP:
339
+ cmd = 1
340
+ elif state == ZoneTransport.States.PLAY:
341
+ cmd = 0
342
+ elif state == ZoneTransport.States.PAUSE:
343
+ cmd = 2
344
+ else:
345
+ return
346
+
347
+ self._log_debug(f"Requesting transports state {state.name}, {state.value}")
348
+ command = self._add_zone_id_to_request(bytearray([16, 61, 2, 0, cmd]))
349
+ self.send(command)
350
+
351
+ #
352
+ # 2A [42]
353
+ # Status Stream Source
354
+ #
355
+ def request_action_2A(self):
356
+ self._log_debug(f"Requesting stream source")
357
+ command = self._add_zone_id_to_request(bytearray([16, 42, 1, 0]))
358
+ self.send(command)
359
+
360
+ #
361
+ # 2D [45]
362
+ # Disable / Enable EQ
363
+ #
364
+ def request_action_2D(self, state: int):
365
+ self._log_debug(f"Requesting EQ Enable: {state}")
366
+ command = self._add_zone_id_to_request(
367
+ bytearray([16, 45, 2, 0, int(not not state)])
368
+ )
369
+ self.send(command)
370
+
371
+ #
372
+ # 33 [51]
373
+ # Reboot
374
+ #
375
+ def request_action_33(self):
376
+ self._log_debug(f"Requesting to reboot single zone")
377
+ command = self._add_zone_id_to_request(bytearray([16, 51, 2, 0, 1]))
378
+ self.send(command)
379
+
380
+ #
381
+ # 33 [51]
382
+ # Reboot All Zones
383
+ #
384
+ def request_action_33_device(self):
385
+ self._log_debug(f"Requesting to reboot device")
386
+ self.send(bytearray([16, 51, 2, 0, 1]))
387
+
388
+ #
389
+ # 47 [71]
390
+ # Set Input Priority
391
+ #
392
+ def request_action_47(self, priority: int):
393
+ self._log_debug(f"Requesting to set input priority {priority}")
394
+ command = self._add_zone_id_to_request(bytearray([16, 71, 2, 0, priority]))
395
+ self.send(command)
396
+
397
+ #
398
+ # 4B [75]
399
+ # Group Add Member
400
+ #
401
+ # In other words: set the zones (zone_index) parent to this zone
402
+ #
403
+ def request_action_4B_add(self, zone_index: int):
404
+ self._log_debug(f"Requesting to add child zone {zone_index} to group")
405
+ command = self._add_zone_id_to_request(bytearray([16, 75, 2, 0, zone_index]))
406
+ self.send(command)
407
+
408
+ #
409
+ # 4B [75]
410
+ # Group Remove Member
411
+ #
412
+ # In other words: set the zones parent to 255
413
+ #
414
+ def request_action_4B_remove(self, zone_index: int):
415
+ self._log_debug(f"Requesting to remove child zone {zone_index} from group")
416
+ # Doesnt need a zone id
417
+ self.send(bytearray([16, 75, 2, 255, zone_index]))
418
+
419
+ #
420
+ # 4B [75]
421
+ # Group Dissolve
422
+ #
423
+ # In other words: set this zones childen to 255
424
+ #
425
+ def request_action_4B_dissolve(self):
426
+ self._log_debug(f"Requesting to Dissolve group")
427
+ command = self._add_zone_id_to_request(bytearray([16, 75, 2, 0, 255]))
428
+ self.send(command)
429
+
430
+ #
431
+ # 55 [85]
432
+ # Play URL
433
+ #
434
+ # Unexpected behaviour on A3.x - feedback is inconsistent
435
+ #
436
+ # There is a bug in the vssl that if we send the volume, it will continually send
437
+ # back volume updates while the url is being played. So for now, we send the current
438
+ # volume and it them seems to behave ok
439
+ #
440
+ #
441
+ """ note: the call will return immediately with either a failure message or an indication that the
442
+ playback has been requested. It is possible for the playback to fail (e.g. the network can't retrieve the file,
443
+ the file format is invalid, ...). Further status will be provided in the coming VSSL FW iterations.
444
+
445
+ note2: if this is the first time a playback has been requested then we will send a command to wake up the unit
446
+ and wait a few seconds before playing the file. Otherwise, if the last playback was less then 15 minutes ago
447
+ then we will play the clip immediately.
448
+
449
+ note3: this call allows you to play a file on 1 or all of the zones. If you want to play to a subset
450
+ (e.g. zone 1,2) then you will need to make two calls, one for each zone you want to play the file on.
451
+
452
+ ref: https://vssl.gitbook.io/vssl-rest-api/announcements/play-audio-file
453
+
454
+ """
455
+
456
+ def request_action_55(self, url: str, all_zones: bool = False):
457
+ string = "PLAYITEM:DIRECT:" + f"{url}"
458
+
459
+ command = bytearray([16, 85])
460
+ command.extend(struct.pack(">B", len(string) + 2))
461
+
462
+ # Zone 0 will play on all zones
463
+ command.extend([0, self.zone.volume])
464
+
465
+ if not all_zones:
466
+ command = self._add_zone_id_to_request(command)
467
+
468
+ command.extend(string.encode("utf-8"))
469
+
470
+ self._log_debug(f"Requesting to play file {url} cmd: {command}")
471
+ self.send(command)
472
+
473
+ #
474
+ # 4F [79]
475
+ # Adaptive Power - Device level Command
476
+ #
477
+ def request_action_4F(self, state=True):
478
+ self._log_debug(f"Requesting to set adaptive power state: {state}")
479
+ # Device level command (dont need zone)
480
+ command = bytearray([16, 79, 2, 8, int(state)])
481
+ self.send(command)
482
+
483
+ #
484
+ #
485
+ #
486
+ # Respsonses
487
+ #
488
+ #
489
+ #
490
+
491
+ async def _read_byte_stream(self, reader, data):
492
+ data += await reader.readexactly(self.HEADER_LENGTH - APIBase.FRIST_BYTE)
493
+ length = data[2]
494
+
495
+ data += await reader.readexactly(length)
496
+
497
+ self._log_debug(f"Response data: {data}")
498
+
499
+ if length == 1:
500
+ return self.response_action_confimation(data)
501
+
502
+ await self._handle_response(data)
503
+
504
+ async def _handle_response(self, response: bytes):
505
+ try:
506
+ # Convert to HEX and split into a array
507
+ hexl = response.hex("-").split("-")
508
+ action = f"response_action_{hexl[1].upper()}"
509
+
510
+ self._log_debug(f"Response action: {action}")
511
+
512
+ except Exception as error:
513
+ self._log_error(f"couldnt handle response: {error} | {hexl}")
514
+ return None
515
+
516
+ if hasattr(self, action):
517
+ method = getattr(self, action)
518
+ if callable(method):
519
+ return method(hexl, response)
520
+
521
+ # Default
522
+ return self.response_action_default(hexl, response)
523
+
524
+ #
525
+ # 00 [0]
526
+ # Received JSON Status Data
527
+ #
528
+ def response_action_00(self, hexl: list, response: bytes):
529
+ try:
530
+ packet_length = hexl[2]
531
+
532
+ length = hex_to_int(packet_length) - 1
533
+ string = response[
534
+ self.JSON_HEADER_LENGTH : self.JSON_HEADER_LENGTH + length
535
+ ].decode("ascii")
536
+ metadata = json.loads(string)
537
+
538
+ # Call a sub action
539
+ sub_action = f"response_action_00_{hexl[3].upper()}"
540
+
541
+ if hasattr(self, sub_action):
542
+ method = getattr(self, sub_action)
543
+ if callable(method):
544
+ self._log_debug(f"Calling status sub action: {sub_action}")
545
+ return method(metadata)
546
+
547
+ self._log_debug(f"Unknown status sub action {sub_action}")
548
+
549
+ except Exception as error:
550
+ self._log_error(f"Couldnt parse JSON: {error} | {hexl}")
551
+
552
+ #
553
+ # 00_00
554
+ # Device Status 00
555
+ #
556
+ # {'B1Src': '3', 'B2Src': '4', 'B3Src': '5', 'B1Nm': '', 'B2Nm': 'Optical In', 'dev': 'Device Name', 'ver': 'p15305.016.3701'}
557
+ #
558
+ def response_action_00_00(self, metadata: list):
559
+ self._log_debug(f"Received 00 Status: {metadata}")
560
+
561
+ # Workout how many zones we have
562
+ self.vssl._infer_model_zone_qty(metadata)
563
+
564
+ # Analog output source
565
+ key = DeviceStatusExtKeys.add_zone_to_bus_key(self.zone.id)
566
+ if key in metadata:
567
+ self.zone.analog_output._set_property("source", int(metadata[key]))
568
+
569
+ # B1Nm - Bus1 Name
570
+ # Not used?
571
+
572
+ # B2Nm - Bus2 Name - For A3.X this is the optical input name
573
+ if DeviceStatusExtKeys.OPTICAL_INPUT_NAME in metadata:
574
+ self.vssl.settings._set_property(
575
+ "optical_input_name",
576
+ metadata[DeviceStatusExtKeys.OPTICAL_INPUT_NAME].strip(),
577
+ )
578
+
579
+ # Set the device name
580
+ if DeviceStatusExtKeys.DEVICE_NAME in metadata:
581
+ self.vssl.settings._set_property(
582
+ "name", metadata[DeviceStatusExtKeys.DEVICE_NAME].strip()
583
+ )
584
+
585
+ # Set the software version
586
+ if DeviceStatusExtKeys.SW_VERSION in metadata and self.vssl.sw_version == None:
587
+ self.vssl._set_property(
588
+ "sw_version", metadata[DeviceStatusExtKeys.SW_VERSION].strip()
589
+ )
590
+
591
+ #
592
+ # 00_08
593
+ # Zone Status 08
594
+ #
595
+ # {'id': '1', 'ac': '0', 'mc': 'XXXXXXXXXXXX', 'vol': '20', 'mt': '0', 'pa': '0', 'rm': '0', 'ts': '14',
596
+ # 'alex': '14', 'nmd': '0', 'ird': '14', 'lb': '24', 'tp': '13', 'wr': '0', 'as': '0', 'rg': '0'}
597
+ #
598
+ def response_action_00_08(self, metadata: list):
599
+ self._log_debug(f"Received 08 Status: {metadata}")
600
+
601
+ # If the zone is not initialised, then we just return the ID and serial
602
+ if not self.zone.initialised:
603
+ # Zone Index
604
+ if ZoneStatusExtKeys.ID in metadata:
605
+ self.zone.id = int(metadata[ZoneStatusExtKeys.ID])
606
+
607
+ # Serial number and MAC address of ZONE 1
608
+ if ZoneStatusExtKeys.SERIAL_NUMBER in metadata:
609
+ # Always set VSSL first before zone
610
+ if self.vssl.serial == None:
611
+ self.vssl._set_property(
612
+ "serial", metadata[ZoneStatusExtKeys.SERIAL_NUMBER]
613
+ )
614
+
615
+ if self.zone.serial == None:
616
+ self.zone._set_property(
617
+ "serial", metadata[ZoneStatusExtKeys.SERIAL_NUMBER]
618
+ )
619
+
620
+ # Transport state
621
+ if ZoneStatusExtKeys.TRANSPORT_STATE in metadata:
622
+ self.zone.transport._set_property(
623
+ "state", int(metadata[ZoneStatusExtKeys.TRANSPORT_STATE])
624
+ )
625
+
626
+ # Volume
627
+ if ZoneStatusExtKeys.VOLUME in metadata:
628
+ self.zone._set_property("volume", int(metadata[ZoneStatusExtKeys.VOLUME]))
629
+
630
+ # Mute
631
+ if ZoneStatusExtKeys.MUTE in metadata:
632
+ self.zone._set_property("mute", bool(int(metadata[ZoneStatusExtKeys.MUTE])))
633
+
634
+ # Party Mode
635
+ # Not supported by X series?
636
+ if ZoneStatusExtKeys.PARTY_MODE in metadata:
637
+ pass
638
+
639
+ # Group Index see below
640
+ if ZoneStatusExtKeys.GROUP_INDEX in metadata:
641
+ self.zone.group._set_property(
642
+ "index", int(metadata[ZoneStatusExtKeys.GROUP_INDEX])
643
+ )
644
+
645
+ # Set Stream Source
646
+ if ZoneStatusExtKeys.TRACK_SOURCE in metadata:
647
+ self.zone.track.source = int(metadata[ZoneStatusExtKeys.TRACK_SOURCE])
648
+
649
+ # Zone Enabled (0) or Disabled (1)
650
+ if ZoneStatusExtKeys.DISABLED in metadata:
651
+ self.zone.settings._set_property(
652
+ "disabled", bool(int(metadata[ZoneStatusExtKeys.DISABLED]))
653
+ )
654
+
655
+ #
656
+ # 00_09
657
+ # EQ Status
658
+ #
659
+ # {'mono': '0', 'AiNm': 'Analog In 1', 'eq1': '100', 'eq2': '100', 'eq3': '100', 'eq4': '100',
660
+ # 'eq5': '100', 'eq6': '100', 'eq7': '100', 'voll': '75', 'volr': '75', 'vold': '0'}
661
+ #
662
+ def response_action_00_09(self, metadata: list):
663
+ self._log_debug(f"Received 09 Status: {metadata}")
664
+
665
+ # Mono output
666
+ if ZoneEQStatusExtKeys.MONO in metadata:
667
+ self.zone.settings._set_property(
668
+ "mono", int(metadata[ZoneEQStatusExtKeys.MONO])
669
+ )
670
+
671
+ # Analog Input Name
672
+ if ZoneEQStatusExtKeys.ANALOG_INPUT_NAME in metadata:
673
+ self.zone.settings.analog_input._set_property(
674
+ "name", metadata[ZoneEQStatusExtKeys.ANALOG_INPUT_NAME].strip()
675
+ )
676
+
677
+ self.zone.settings.eq._map_response_dict(metadata)
678
+ self.zone.settings.volume._map_response_dict(metadata)
679
+
680
+ #
681
+ # 00_0A
682
+ # System Status 0A (Output / Amp)
683
+ #
684
+ # {'ECO': '0', 'eqsw': '1', 'inSrc': '0', 'SP': '0', 'BF1': '0', 'BF2': '0', 'BF3': '0',
685
+ # 'GRM': '0', 'GRS': '255', 'Pwr': '0', 'Bvr': '1', 'fxv': '24', 'AtPwr': '1'}
686
+ #
687
+ def response_action_00_0A(self, metadata: list):
688
+ self._log_debug(f"Received 0A Status: {metadata}")
689
+
690
+ # EQ Switch
691
+ if ZoneRouterStatusExtKeys.EQ_ENABLED in metadata:
692
+ self.zone.settings.eq._set_property(
693
+ "enabled", bool(int(metadata[ZoneRouterStatusExtKeys.EQ_ENABLED]))
694
+ )
695
+
696
+ # Input Source
697
+ if ZoneRouterStatusExtKeys.INPUT_SOURCE in metadata:
698
+ self.zone.input._set_property(
699
+ "source", int(metadata[ZoneRouterStatusExtKeys.INPUT_SOURCE])
700
+ )
701
+
702
+ # Source Priority
703
+ if ZoneRouterStatusExtKeys.SOURCE_PRIORITY in metadata:
704
+ self.zone.input._set_property(
705
+ "priority", int(metadata[ZoneRouterStatusExtKeys.SOURCE_PRIORITY])
706
+ )
707
+
708
+ # Analog Output Fix Volume
709
+ # e.g BF1
710
+ key = ZoneRouterStatusExtKeys.add_zone_to_ao_fixed_volume_key(self.zone.id)
711
+ if key in metadata:
712
+ self.zone.analog_output._set_property(
713
+ "is_fixed_volume", bool(int(metadata[key]))
714
+ )
715
+
716
+ # Handle groups
717
+ if (
718
+ ZoneRouterStatusExtKeys.GROUP_MASTER in metadata
719
+ and ZoneRouterStatusExtKeys.GROUP_SOURCE in metadata
720
+ ):
721
+ self.zone.group._set_property(
722
+ "source", int(metadata[ZoneRouterStatusExtKeys.GROUP_SOURCE])
723
+ )
724
+ self.zone.group._set_property(
725
+ "is_master", int(metadata[ZoneRouterStatusExtKeys.GROUP_MASTER])
726
+ )
727
+
728
+ # Power State
729
+ if ZoneRouterStatusExtKeys.POWER_STATE in metadata:
730
+ self.vssl.settings.power._set_property(
731
+ "state", int(metadata[ZoneRouterStatusExtKeys.POWER_STATE])
732
+ )
733
+
734
+ # Analog input fixed gain
735
+ if ZoneRouterStatusExtKeys.ANALOG_INPUT_FIXED_GAIN in metadata:
736
+ self.zone.settings.analog_input._set_property(
737
+ "fixed_gain",
738
+ int(metadata[ZoneRouterStatusExtKeys.ANALOG_INPUT_FIXED_GAIN]),
739
+ )
740
+
741
+ # Alway On power state = 0 else 1 = auto
742
+ if ZoneRouterStatusExtKeys.ADAPTIVE_POWER in metadata:
743
+ self.vssl.settings.power._set_property(
744
+ "adaptive", bool(int(metadata[ZoneRouterStatusExtKeys.ADAPTIVE_POWER]))
745
+ )
746
+
747
+ #
748
+ # 00_0B
749
+ # System Status 0B (Not Sure)
750
+ #
751
+ # {'IRMskL': '241', 'IRMskH': '255', 'BTSta': '0', 'Crs': '0', 'Fes': '0', 'Drk': '0'}
752
+ #
753
+ #
754
+ # IRMskL / IRMskH:
755
+ # These could potentially be related to Infrared (IR) remote control signals. "IRMskL" and "IRMskH" might represent the low and high values of the modulation frequency or pulse width for an infrared signal.
756
+ #
757
+ # BTSta:
758
+ # This might represent the Bluetooth status, with "0" indicating that Bluetooth is currently not active or disconnected.
759
+ #
760
+ # Crs:
761
+ # It could stand for "Crossfade" and may represent a setting related to crossfading between audio tracks.
762
+ #
763
+ # Fes:
764
+ # This might stand for "Frequency" or "Filter Effect Setting," representing a parameter related to frequency or filtering effects.
765
+ #
766
+ # Drk: ?
767
+ #
768
+ def response_action_00_0B(self, metadata: list):
769
+ self._log_debug(f"Received 0B Status: {metadata}")
770
+ pass
771
+
772
+ #
773
+ # 2A [42]
774
+ # Stream Source
775
+ #
776
+ def response_action_2A(self, hexl: list, response: bytes):
777
+ if hex_to_int(hexl[2]) == 2:
778
+ source = hex_to_int(hexl[4])
779
+ self._log_debug(f"Received stream source: {source}")
780
+ self.zone.track.source = source
781
+
782
+ #
783
+ # 1E [30]
784
+ # Received Analog Output Source Change
785
+ #
786
+ # Note: This is received on the zone which is the same as the output ID
787
+ #
788
+ def response_action_1E(self, hexl: list, response: bytes):
789
+ if hex_to_int(hexl[2]) == 2:
790
+ output = hex_to_int(hexl[3])
791
+ source = hex_to_int(hexl[4])
792
+ self._log_debug(f"Received analog output {output} source change: {source}")
793
+ self.zone.analog_output._set_property("source", source)
794
+
795
+ #
796
+ # 4A [74]
797
+ # Analog Output Fix Output Vol
798
+ #
799
+ # Note: This is received on the zone which is the same as the output ID
800
+ #
801
+ def response_action_4A(self, hexl: list, response: bytes):
802
+ if hex_to_int(hexl[2]) == 2:
803
+ output = hex_to_int(hexl[3])
804
+ state = hex_to_int(hexl[4])
805
+ self._log_debug(f"Received analog output {output} volume fixed: {state}")
806
+ self.zone.analog_output._set_property("is_fixed_volume", bool(state))
807
+
808
+ #
809
+ # 04 [4]
810
+ # Received Input Source
811
+ #
812
+ def response_action_04(self, hexl: list, response: bytes):
813
+ if hex_to_int(hexl[2]) == 2:
814
+ source = hex_to_int(hexl[4])
815
+ self._log_debug(f"Received input source: {source}")
816
+ self.zone.input._set_property("source", source)
817
+
818
+ #
819
+ # 06 [6]
820
+ # Received Volume Data
821
+ #
822
+ def response_action_06(self, hexl: list, response: bytes):
823
+ if hex_to_int(hexl[2]) == 3:
824
+ vol = hex_to_int(hexl[4])
825
+ vol_cmd = hex_to_int(hexl[5])
826
+
827
+ self._log_debug(f"Received volume cmd: {vol_cmd} vol: {vol}")
828
+ self._log_debug(f"Received volume {response.hex()}")
829
+
830
+ # Analog input fixed gain
831
+ if vol_cmd == 0:
832
+ self.zone.settings.analog_input._set_property("fixed_gain", vol)
833
+
834
+ # Max Left
835
+ elif vol_cmd == 1:
836
+ self.zone.settings.volume._set_property("max_left", vol)
837
+
838
+ # Max Right
839
+ elif vol_cmd == 2:
840
+ self.zone.settings.volume._set_property("max_right", vol)
841
+
842
+ # Normal Volume Change
843
+ elif vol_cmd == 3:
844
+ self.zone._set_property("volume", vol)
845
+
846
+ # Defaul On Volume Change
847
+ elif vol_cmd == 8:
848
+ self.zone.settings.volume._set_property("default_on", vol)
849
+ else:
850
+ self._log_debug(f"Volume Error")
851
+
852
+ #
853
+ # 07 [7]
854
+ # Transport State
855
+ #
856
+ # 0 = stop
857
+ # 1 = play
858
+ # 2 = pause
859
+ #
860
+ def response_action_07(self, hexl: list, response: bytes):
861
+ if hex_to_int(hexl[2]) == 2:
862
+ state = hex_to_int(hexl[4])
863
+ self._log_debug(f"Received transport state: {state}")
864
+ self.zone.transport._set_property("state", state)
865
+
866
+ #
867
+ # 0B [11]
868
+ # Party Mode not supported on X series (I think. TODO)
869
+ #
870
+ def response_action_0B(self, hexl: list, response: bytes):
871
+ pass
872
+
873
+ #
874
+ # 0E [14]
875
+ # EQ
876
+ #
877
+ def response_action_0E(self, hexl: list, response: bytes):
878
+ if hex_to_int(hexl[2]) == 3:
879
+ freq = hex_to_int(hexl[4])
880
+ value = hex_to_int(hexl[5])
881
+ self._log_debug(f"Received EQ requency:{freq} value: {value}")
882
+ self.zone.settings.eq._set_eq_freq(freq, value)
883
+
884
+ #
885
+ # 10 [16]
886
+ # Mono output
887
+ #
888
+ def response_action_10(self, hexl: list, response: bytes):
889
+ if hex_to_int(hexl[2]) == 2:
890
+ state = hex_to_int(hexl[4])
891
+ self._log_debug(f"Received mono ouput: {state}")
892
+ self.zone.settings._set_property("mono", state)
893
+
894
+ #
895
+ # 12 [18]
896
+ # Mute status
897
+ #
898
+ def response_action_12(self, hexl: list, response: bytes):
899
+ if hex_to_int(hexl[2]) == 2:
900
+ is_muted = bool(hex_to_int(hexl[4]))
901
+ self._log_debug(f"Received mute status 12: {is_muted}")
902
+ self.zone._set_property("mute", is_muted)
903
+
904
+ #
905
+ # 16 [22]
906
+ # Received Analog Input Name
907
+ #
908
+ # TODO, maybe this should be global with the analog outputs
909
+ #
910
+ def response_action_16(self, hexl: list, response: bytes):
911
+ self._log_debug(f"Received analog input name: {hexl}")
912
+
913
+ input_id = hex_to_int(hexl[3])
914
+ name = response[4:].decode("ascii")
915
+
916
+ if input_id == self.zone.id:
917
+ self._log_debug(f"Received analog input {input_id} name: {name}")
918
+ self.zone.settings.analog_input._set_property("name", name.strip())
919
+
920
+ # Optical Input
921
+ elif input_id == 12:
922
+ self._log_debug(f"Received optical input name: {name}")
923
+ self.vssl.settings._set_property("optical_input_name", name.strip())
924
+
925
+ #
926
+ # 17 [23]
927
+ # Received Keep Alive
928
+ #
929
+ def response_action_17(self, response: bytes):
930
+ self._log_debug(f"Z{self.zone.id} Alpha - Received keep alive: {response}")
931
+ # TODO
932
+ pass
933
+
934
+ #
935
+ # 19 [25]
936
+ # Received Device Name
937
+ #
938
+ def response_action_19(self, hexl: list, response: bytes):
939
+ try:
940
+ length = hex_to_int(hexl[2]) - 1
941
+ name = response[
942
+ self.JSON_HEADER_LENGTH : self.JSON_HEADER_LENGTH + length
943
+ ].decode("ascii")
944
+
945
+ self._log_debug(f"Received device name: {name}")
946
+
947
+ self.vssl.settings._set_property("name", name.strip())
948
+
949
+ except Exception as error:
950
+ self._log_error(f"Exception occurred receiving device name: {error}")
951
+
952
+ #
953
+ # 26 [38]
954
+ # Zone Enabled / Disabled Feedback
955
+ #
956
+ def response_action_26(self, hexl: list, response: bytes):
957
+ if hex_to_int(hexl[2]) == 3:
958
+ # hexl[4] is the zone id
959
+ disabled = hex_to_int(hexl[5])
960
+ self._log_debug(f"Received zone disable: {disabled}")
961
+ self.zone.settings._set_property("disabled", bool(disabled))
962
+
963
+ #
964
+ # 2E [46]
965
+ # EQ Switch
966
+ #
967
+ def response_action_2E(self, hexl: list, response: bytes):
968
+ if hex_to_int(hexl[2]) == 2:
969
+ enabled = hex_to_int(hexl[4])
970
+ self._log_debug(f"Received EQ Switch: {enabled}")
971
+ self.zone.settings.eq._set_property("enabled", bool(enabled))
972
+
973
+ #
974
+ # 32 [50]
975
+ # = 'rm' key in the status object.
976
+ #
977
+ # A int is assigned to the zone when it starts playing.
978
+ # When a zone joins a group it will allocated the same 'rm' number.
979
+ #
980
+ # When a stream is started, a 'rm' is allocated to the zone.
981
+ # RM feedback is 0 when not playing and removed from a group
982
+ def response_action_32(self, hexl: list, response: bytes):
983
+ if hex_to_int(hexl[2]) == 2:
984
+ index = hex_to_int(hexl[4])
985
+ self._log_debug(f"Received group index: {index}")
986
+ self.zone.group._set_property("index", int(index))
987
+
988
+ #
989
+ # 4C [76]
990
+ # Group Response
991
+ #
992
+ def response_action_4C(self, hexl: list, response: bytes):
993
+ self._log_debug(f"Received group info: {hexl}")
994
+
995
+ if hex_to_int(hexl[2]) == 3:
996
+ if hex_to_int(hexl[3]) != self.zone.id:
997
+ self._log_warning(
998
+ f"Z{self.zone.id} Alpha - incorrect zone id in group response"
999
+ )
1000
+ return
1001
+
1002
+ self.zone.group._set_property("source", hex_to_int(hexl[5]))
1003
+ self.zone.group._set_property("is_master", hex_to_int(hexl[4]))
1004
+
1005
+ #
1006
+ # 48 [72]
1007
+ # Input Priority Feedback
1008
+ #
1009
+ def response_action_48(self, hexl: list, response: bytes):
1010
+ if hex_to_int(hexl[2]) == 2:
1011
+ priority = hex_to_int(hexl[4])
1012
+ self._log_debug(f"Received input priority: {priority}")
1013
+ self.zone.input._set_property("priority", priority)
1014
+
1015
+ #
1016
+ # 50 [80]
1017
+ # Adaptive Power Feedback
1018
+ #
1019
+ def response_action_50(self, hexl: list, response: bytes):
1020
+ if hex_to_int(hexl[2]) == 2:
1021
+ enabled = hex_to_int(hexl[4])
1022
+ self._log_debug(f"Received adaptive power setting: {enabled}")
1023
+ self.vssl.settings.power._set_property("adaptive", bool(int(enabled)))
1024
+
1025
+ #
1026
+ # Command confimation
1027
+ #
1028
+ def response_action_confimation(self, response: bytes):
1029
+ self._log_debug(
1030
+ f"Received command confimation: {response[1]}. Hex: {response.hex()}"
1031
+ )
1032
+
1033
+ #
1034
+ # Default
1035
+ # Default Action
1036
+ #
1037
+ def response_action_default(self, hexl: list, response: bytes):
1038
+ self._log_debug(
1039
+ f"Received unknown command {hexl[1].upper()}. Hex: {response.hex()}"
1040
+ )