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/settings.py ADDED
@@ -0,0 +1,655 @@
1
+ import logging
2
+ from typing import Dict, Union
3
+ from .utils import clamp_volume
4
+ from .io import AnalogInput
5
+ from .data_structure import (
6
+ VsslIntEnum,
7
+ VsslDataClass,
8
+ ZoneDataClass,
9
+ ZoneEQStatusExtKeys,
10
+ )
11
+
12
+ VSSL_SETTINGS_EVENT_PREFIX = "vssl.settings."
13
+
14
+
15
+ class VsslSettings(VsslDataClass):
16
+ class Keys:
17
+ NAME = "name"
18
+ OPTICAL_INPUT_NAME = "optical_input_name"
19
+
20
+ #
21
+ # VSSL Events
22
+ #
23
+ class Events:
24
+ PREFIX = VSSL_SETTINGS_EVENT_PREFIX
25
+ NAME_CHANGE = PREFIX + "name_changed"
26
+ OPTICAL_INPUT_NAME_CHANGE = PREFIX + "optical_input_name_changed"
27
+
28
+ #
29
+ # Defaults
30
+ #
31
+ DEFAULTS = {Keys.NAME: None, Keys.OPTICAL_INPUT_NAME: "Optical In"}
32
+
33
+ def __init__(self, vssl: "vsslctrl.Vssl"):
34
+ self._vssl = vssl
35
+
36
+ self._name = None # device name
37
+ self._optical_input_name = self.DEFAULTS[self.Keys.OPTICAL_INPUT_NAME]
38
+ self.power = VsslPowerSettings(vssl)
39
+
40
+ #
41
+ # Name
42
+ #
43
+ @property
44
+ def name(self):
45
+ return self._name
46
+
47
+ @name.setter
48
+ def name(self, name: str):
49
+ zone = self._vssl.get_connected_zone()
50
+ if zone and name:
51
+ zone.api_alpha.request_action_18(name)
52
+
53
+ #
54
+ # Optical Input Name
55
+ #
56
+ @property
57
+ def optical_input_name(self):
58
+ return self._optical_input_name
59
+
60
+ @optical_input_name.setter
61
+ def optical_input_name(self, name: str):
62
+ zone = self._vssl.get_connected_zone()
63
+ if zone:
64
+ zone.api_alpha.request_action_15_12(name)
65
+
66
+
67
+ class VsslPowerSettings(VsslDataClass):
68
+ #
69
+ # Transport States
70
+ #
71
+ # DO NOT CHANGE - VSSL Defined
72
+ #
73
+ class States(VsslIntEnum):
74
+ ON = 0
75
+ STANDBY = 1
76
+ SLEEP = 2
77
+
78
+ #
79
+ # Volume Setting Events
80
+ #
81
+ class Events:
82
+ PREFIX = VSSL_SETTINGS_EVENT_PREFIX + "power."
83
+ STATE_CHANGE = PREFIX + "state_changed"
84
+ ADAPTIVE_CHANGE = PREFIX + "adaptive_changed"
85
+
86
+ #
87
+ # Defaults
88
+ #
89
+ DEFAULTS = {"state": States.ON, "adaptive": False}
90
+
91
+ def __init__(self, vssl: "vsslctrl.Vssl"):
92
+ self._vssl = vssl
93
+
94
+ self._state = self.States.ON
95
+ self._adaptive = True # 1 = auto, 0 = always on
96
+
97
+ #
98
+ # Power State
99
+ #
100
+ @property
101
+ def state(self):
102
+ return self._state
103
+
104
+ @state.setter
105
+ def state(self, state: int):
106
+ # read-only
107
+ pass
108
+
109
+ def _set_state(self, state: int):
110
+ if self.state != state:
111
+ if self.States.is_valid(state):
112
+ self._state = self.States(state)
113
+ return True
114
+ else:
115
+ self._vssl._log_warning(
116
+ f"VsslPowerSettings.States {state} doesnt exist"
117
+ )
118
+
119
+ #
120
+ # Adaptive Power (always on or auto)
121
+ #
122
+ # 1 = Auto / Adaptive Power On
123
+ # 0 = Always On
124
+ #
125
+ @property
126
+ def adaptive(self):
127
+ return bool(self._adaptive)
128
+
129
+ @adaptive.setter
130
+ def adaptive(self, enabled: bool):
131
+ zone = self._vssl.get_connected_zone()
132
+ if zone:
133
+ zone.api_alpha.request_action_4F(not not enabled)
134
+
135
+ def adaptive_toggle(self):
136
+ self.adaptive = False if self.adaptive else True
137
+
138
+
139
+ class ZoneSettings(ZoneDataClass):
140
+ class StereoMono(VsslIntEnum):
141
+ """StereoMono
142
+
143
+ 0: Stereo
144
+ 1: Mono
145
+
146
+ DO NOT CHANGE - VSSL Defined
147
+ """
148
+
149
+ Stereo = 0
150
+ Mono = 1
151
+
152
+ class Events:
153
+ """Setting Events"""
154
+
155
+ PREFIX = "zone.settings."
156
+ DISABLED_CHANGE = PREFIX + "disabled_changed"
157
+ NAME_CHANGE = PREFIX + "name_changed"
158
+ MONO_CHANGE = PREFIX + "mono_changed"
159
+
160
+ def __init__(self, zone: "zone.Zone"):
161
+ self.zone = zone
162
+
163
+ self._disabled = False
164
+ self._name = f"Zone {zone.id}"
165
+ self._mono = False
166
+
167
+ self.eq = EQSettings(zone)
168
+ self.volume = VolumeSettings(zone)
169
+ self.analog_input = AnalogInput(zone)
170
+
171
+ #
172
+ # Disable the zone
173
+ #
174
+ @property
175
+ def disabled(self):
176
+ return self._disabled
177
+
178
+ @disabled.setter
179
+ def disabled(self, disabled: Union[bool, int]):
180
+ self.zone.api_alpha.request_action_25(not not disabled)
181
+
182
+ def disabled_toggle(self):
183
+ self.disabled = False if self.disabled else True
184
+
185
+ #
186
+ # Name
187
+ #
188
+ @property
189
+ def name(self):
190
+ return self._name
191
+
192
+ @name.setter
193
+ def name(self, name: str):
194
+ self.zone.api_bravo.request_action_5A_set(str(name))
195
+ # Incase for some network reason _request_name arrives before the change
196
+ # the setter will remove the request once set
197
+ self.zone._poller.append(self.zone._request_name)
198
+ self.zone._request_name()
199
+
200
+ def _set_name(self, name: str):
201
+ if name != self._name:
202
+ self._name = name
203
+ self.zone._poller.remove(self.zone._request_name)
204
+ return True
205
+
206
+ #
207
+ # Mono
208
+ # Set the output to mono
209
+ #
210
+ @property
211
+ def mono(self):
212
+ return self._mono
213
+
214
+ @mono.setter
215
+ def mono(self, mono: "ZoneSettings.StereoMono"):
216
+ if self.StereoMono.is_valid(mono):
217
+ self.zone.api_alpha.request_action_mono_set(mono)
218
+ else:
219
+ self.zone._log_error(f"ZoneSettings.StereoMono {mono} doesnt exist")
220
+
221
+ def _set_mono(self, mono: int):
222
+ if self.mono != mono:
223
+ if self.StereoMono.is_valid(mono):
224
+ self._mono = self.StereoMono(mono)
225
+ return True
226
+ else:
227
+ self.zone._log_error(f"ZoneSettings.StereoMono {mono} doesnt exist")
228
+
229
+ def mono_toggle(self):
230
+ self.mono = (
231
+ self.StereoMono.Stereo
232
+ if self.mono == self.StereoMono.Mono
233
+ else self.StereoMono.Mono
234
+ )
235
+
236
+
237
+ class VolumeSettings(ZoneDataClass):
238
+ class Keys:
239
+ DEFAULT_ON = "default_on"
240
+ MAX_LEFT = "max_left"
241
+ MAX_RIGHT = "max_right"
242
+
243
+ class Events:
244
+ """Volume Setting Events"""
245
+
246
+ PREFIX = "zone.settings.volume."
247
+ DEFAULT_ON_CHANGE = PREFIX + "default_on_changed"
248
+ MAX_LEFT_CHANGE = PREFIX + "max_left_changed"
249
+ MAX_RIGHT_CHANGE = PREFIX + "max_right_changed"
250
+
251
+ #
252
+ # Defaults
253
+ #
254
+ DEFAULTS = {
255
+ Keys.DEFAULT_ON: 0,
256
+ Keys.MAX_LEFT: 75,
257
+ Keys.MAX_RIGHT: 75,
258
+ }
259
+
260
+ #
261
+ # Key Map for the reponse JSON to our internal structure
262
+ #
263
+ KEY_MAP = {
264
+ ZoneEQStatusExtKeys.VOL_DEFAULT_ON: Keys.DEFAULT_ON,
265
+ ZoneEQStatusExtKeys.VOL_MAX_LEFT: Keys.MAX_LEFT,
266
+ ZoneEQStatusExtKeys.VOL_MAX_RIGHT: Keys.MAX_RIGHT,
267
+ }
268
+
269
+ def __init__(self, zone: "zone.Zone"):
270
+ self.zone = zone
271
+
272
+ self._default_on = self.DEFAULTS[self.Keys.DEFAULT_ON]
273
+ self._max_left = self.DEFAULTS[self.Keys.MAX_LEFT]
274
+ self._max_right = self.DEFAULTS[self.Keys.MAX_RIGHT]
275
+
276
+ #
277
+ # Update from a JSON dict passed
278
+ #
279
+ def _map_response_dict(self, volume_data: Dict[str, int]) -> None:
280
+ for volume_data_key, settings_prop in self.KEY_MAP.items():
281
+ if volume_data_key in volume_data:
282
+ set_func = f"_set_{settings_prop}"
283
+ if hasattr(self, set_func):
284
+ getattr(self, set_func)(volume_data[volume_data_key])
285
+
286
+ #
287
+ # Default On Volume
288
+ #
289
+ # When enabled, volume automatically reverts to the determined volume level
290
+ # when initiating a new audio session
291
+ #
292
+ # 0 is off (no default on vol)
293
+ #
294
+ @property
295
+ def default_on(self):
296
+ return self._default_on
297
+
298
+ @default_on.setter
299
+ def default_on(self, vol: int):
300
+ self.zone.api_alpha.request_action_05_08(vol)
301
+
302
+ def _set_default_on(self, vol: int):
303
+ vol = clamp_volume(vol)
304
+ if self.default_on != vol:
305
+ self._default_on = vol
306
+ return True
307
+
308
+ #
309
+ # Max Left Volume
310
+ #
311
+ @property
312
+ def max_left(self):
313
+ return self._max_left
314
+
315
+ @max_left.setter
316
+ def max_left(self, vol: int):
317
+ self.zone.api_alpha.request_action_05_01(vol)
318
+
319
+ def _set_max_left(self, vol: int):
320
+ vol = clamp_volume(vol)
321
+ if self.max_left != vol:
322
+ self._max_left = vol
323
+ return True
324
+
325
+ #
326
+ # Max Right Volume
327
+ #
328
+ @property
329
+ def max_right(self):
330
+ return self._max_right
331
+
332
+ @max_right.setter
333
+ def max_right(self, vol: int):
334
+ self.zone.api_alpha.request_action_05_02(vol)
335
+
336
+ def _set_max_right(self, vol: int):
337
+ vol = clamp_volume(vol)
338
+ if self.max_right != vol:
339
+ self._max_right = vol
340
+ return True
341
+
342
+
343
+ class EQSettings(ZoneDataClass):
344
+ MIN_VALUE = 90
345
+ MAX_VALUE = 110
346
+ MIN_VALUE_DB = -10
347
+ MAX_VALUE_DB = 10
348
+
349
+ class Keys:
350
+ ENABLED = "enabled"
351
+ HZ60 = "hz60" # 60Hz
352
+ HZ200 = "hz200" # 200Hz
353
+ HZ500 = "hz500" # 500Hz
354
+ KHZ1 = "khz1" # 1kHz
355
+ KHZ4 = "khz4" # 4kHz
356
+ KHZ8 = "khz8" # 8kHz
357
+ KHZ15 = "khz15" # 15kHz
358
+
359
+ #
360
+ # EQ Frequencies
361
+ #
362
+ # DO NOT CHANGE - VSSL Defined
363
+ #
364
+ class Freqs(VsslIntEnum):
365
+ HZ60 = 1 # 60Hz
366
+ HZ200 = 2 # 200Hz
367
+ HZ500 = 3 # 500Hz
368
+ KHZ1 = 4 # 1kHz
369
+ KHZ4 = 5 # 4kHz
370
+ KHZ8 = 6 # 8kHz
371
+ KHZ15 = 7 # 15kHz
372
+
373
+ #
374
+ # Volume Setting Events
375
+ #
376
+ class Events:
377
+ PREFIX = "zone.settings.eq."
378
+ ENABLED_CHANGE = PREFIX + "enabled_change"
379
+ HZ60_CHANGE = PREFIX + "hz60_change"
380
+ HZ200_CHANGE = PREFIX + "hz200_change"
381
+ HZ500_CHANGE = PREFIX + "hz500_change"
382
+ KHZ1_CHANGE = PREFIX + "khz1_change"
383
+ KHZ4_CHANGE = PREFIX + "khz4_change"
384
+ KHZ8_CHANGE = PREFIX + "khz8_change"
385
+ KHZ15_CHANGE = PREFIX + "khz15_change"
386
+
387
+ #
388
+ # Defaults
389
+ #
390
+ DEFAULTS = {
391
+ Keys.ENABLED: False,
392
+ Keys.HZ60: 100,
393
+ Keys.HZ200: 100,
394
+ Keys.HZ500: 100,
395
+ Keys.KHZ1: 100,
396
+ Keys.KHZ4: 100,
397
+ Keys.KHZ8: 100,
398
+ Keys.KHZ15: 100,
399
+ }
400
+
401
+ #
402
+ # Key Map
403
+ #
404
+ KEY_MAP = {
405
+ ZoneEQStatusExtKeys.HZ60: Keys.HZ60,
406
+ ZoneEQStatusExtKeys.HZ200: Keys.HZ200,
407
+ ZoneEQStatusExtKeys.HZ500: Keys.HZ500,
408
+ ZoneEQStatusExtKeys.KHZ1: Keys.KHZ1,
409
+ ZoneEQStatusExtKeys.KHZ4: Keys.KHZ4,
410
+ ZoneEQStatusExtKeys.KHZ8: Keys.KHZ8,
411
+ ZoneEQStatusExtKeys.KHZ15: Keys.KHZ15,
412
+ }
413
+
414
+ def __init__(self, zone: "zone.Zone"):
415
+ self.zone = zone
416
+
417
+ self._enabled = self.DEFAULTS[self.Keys.ENABLED]
418
+ self._hz60 = self.DEFAULTS[self.Keys.HZ60]
419
+ self._hz200 = self.DEFAULTS[self.Keys.HZ200]
420
+ self._hz500 = self.DEFAULTS[self.Keys.HZ500]
421
+ self._khz1 = self.DEFAULTS[self.Keys.KHZ1]
422
+ self._khz4 = self.DEFAULTS[self.Keys.KHZ4]
423
+ self._khz8 = self.DEFAULTS[self.Keys.KHZ8]
424
+ self._khz15 = self.DEFAULTS[self.Keys.KHZ15]
425
+
426
+ def as_dict(self, with_db=True):
427
+ settings = dict(self)
428
+
429
+ if with_db:
430
+ for json_data_key, setting_key in self.KEY_MAP.items():
431
+ key = setting_key + "_db"
432
+ settings[key] = getattr(self, key)
433
+
434
+ return settings
435
+
436
+ #
437
+ # Clamp betwwen 90 and 110
438
+ #
439
+ def _clamp(self, value: int = 100):
440
+ return int(max(self.MIN_VALUE, min(value, self.MAX_VALUE)))
441
+
442
+ #
443
+ # Map between -10 and +10
444
+ #
445
+ # 90 = -10db
446
+ # 100 = 0db
447
+ # 110 = +10db
448
+ #
449
+ def _map_clamp(self, input_value: int = 0, to_db: bool = True):
450
+ """Convert / Map values between 90 & 110 to their -10db & +10db equilivents"""
451
+
452
+ if to_db:
453
+ # Ensure the mapped value is clamped between 90 and 110
454
+ clamped_value = self._clamp(input_value)
455
+
456
+ # Map the clamped value to the range -10 to 10
457
+ return int(
458
+ ((clamped_value - self.MIN_VALUE) / (self.MAX_VALUE - self.MIN_VALUE))
459
+ * 20
460
+ - self.MAX_VALUE_DB
461
+ )
462
+ else:
463
+ # Map the input value from the range -10 to 10 to the range 90 to 110
464
+ mapped_value = ((input_value + self.MAX_VALUE_DB) / 20) * (
465
+ self.MAX_VALUE - self.MIN_VALUE
466
+ ) + self.MIN_VALUE
467
+
468
+ # Ensure the result is clamped between 90 and 110
469
+ return self._clamp(mapped_value)
470
+
471
+ #
472
+ # Set EQ Value
473
+ #
474
+ # Expects a value between: 90 to 110
475
+ #
476
+ def _set_frequency_on_device(self, freq: "EQSettings.Freqs", value: int):
477
+ if self.Freqs.is_valid(freq):
478
+ self.zone.api_alpha.request_action_0D(freq, self._clamp(value))
479
+ else:
480
+ self.zone._log_error(f"EQSettings.Freqs {freq} doesnt exist")
481
+
482
+ #
483
+ # Set EQ Value, using dB as an input
484
+ #
485
+ # Expects: -10 to +10
486
+ #
487
+ def _set_frequency_on_device_db(self, freq: int, value: int):
488
+ self._set_frequency_on_device(freq, self._map_clamp(value, False))
489
+
490
+ #
491
+ # Updade a property and emit and event if changed
492
+ #
493
+ def _set_eq_freq(self, freq: int, new_value: int):
494
+ if self.Freqs.is_valid(freq):
495
+ freq = self.Freqs(freq)
496
+ self._set_property(getattr(self.Keys, freq.name), self._clamp(new_value))
497
+ else:
498
+ self.zone._log_error(f"EQSettings.Freqs {freq} doesnt exist")
499
+
500
+ #
501
+ # Update from a JSON dict passed
502
+ #
503
+ def _map_response_dict(self, json_data: Dict[str, int]) -> None:
504
+ for json_data_key, setting_key in self.KEY_MAP.items():
505
+ if json_data_key in json_data:
506
+ self._set_property(
507
+ setting_key, self._clamp(int(json_data[json_data_key]))
508
+ )
509
+
510
+ #
511
+ # EQ Enabled
512
+ #
513
+ @property
514
+ def enabled(self):
515
+ return self._enabled
516
+
517
+ @enabled.setter
518
+ def enabled(self, enabled: Union[bool, int]):
519
+ self.zone.api_alpha.request_action_2D(enabled)
520
+
521
+ def enabled_toggle(self):
522
+ self.enabled = False if self.enabled else True
523
+
524
+ #
525
+ # EQ 60Hz
526
+ #
527
+ @property
528
+ def hz60(self):
529
+ return self._hz60
530
+
531
+ @hz60.setter
532
+ def hz60(self, val: int):
533
+ self._set_frequency_on_device(self.Freqs.HZ60, val)
534
+
535
+ @property
536
+ def hz60_db(self):
537
+ return self._map_clamp(self.hz60)
538
+
539
+ @hz60_db.setter
540
+ def hz60_db(self, val: int):
541
+ self._set_frequency_on_device_db(self.Freqs.HZ60, val)
542
+
543
+ #
544
+ # EQ 200Hz
545
+ #
546
+ @property
547
+ def hz200(self):
548
+ return self._hz200
549
+
550
+ @hz200.setter
551
+ def hz200(self, val: int):
552
+ self._set_frequency_on_device(self.Freqs.HZ200, val)
553
+
554
+ @property
555
+ def hz200_db(self):
556
+ return self._map_clamp(self.hz200)
557
+
558
+ @hz200_db.setter
559
+ def hz200_db(self, val: int):
560
+ self._set_frequency_on_device_db(self.Freqs.HZ200, val)
561
+
562
+ #
563
+ # EQ 500Hz
564
+ #
565
+ @property
566
+ def hz500(self):
567
+ return self._hz500
568
+
569
+ @hz500.setter
570
+ def hz500(self, val: int):
571
+ self._set_frequency_on_device(self.Freqs.HZ500, val)
572
+
573
+ @property
574
+ def hz500_db(self):
575
+ return self._map_clamp(self.hz500)
576
+
577
+ @hz500_db.setter
578
+ def hz500_db(self, val: int):
579
+ self._set_frequency_on_device_db(self.Freqs.HZ500, val)
580
+
581
+ #
582
+ # EQ 1kHz
583
+ #
584
+ @property
585
+ def khz1(self):
586
+ return self._khz1
587
+
588
+ @khz1.setter
589
+ def khz1(self, val: int):
590
+ self._set_frequency_on_device(self.Freqs.KHZ1, val)
591
+
592
+ @property
593
+ def khz1_db(self):
594
+ return self._map_clamp(self.khz1)
595
+
596
+ @khz1_db.setter
597
+ def khz1_db(self, val: int):
598
+ self._set_frequency_on_device_db(self.Freqs.KHZ1, val)
599
+
600
+ #
601
+ # EQ 4kHz
602
+ #
603
+ @property
604
+ def khz4(self):
605
+ return self._khz4
606
+
607
+ @khz4.setter
608
+ def khz4(self, val: int):
609
+ self._set_frequency_on_device(self.Freqs.KHZ4, val)
610
+
611
+ @property
612
+ def khz4_db(self):
613
+ return self._map_clamp(self.khz4)
614
+
615
+ @khz4_db.setter
616
+ def khz4_db(self, val: int):
617
+ self._set_frequency_on_device_db(self.Freqs.KHZ4, val)
618
+
619
+ #
620
+ # EQ 8kHz
621
+ #
622
+ @property
623
+ def khz8(self):
624
+ return self._khz8
625
+
626
+ @khz8.setter
627
+ def khz8(self, val: int):
628
+ self._set_frequency_on_device(self.Freqs.KHZ8, val)
629
+
630
+ @property
631
+ def khz8_db(self):
632
+ return self._map_clamp(self.khz8)
633
+
634
+ @khz8_db.setter
635
+ def khz8_db(self, val: int):
636
+ self._set_frequency_on_device_db(self.Freqs.KHZ8, val)
637
+
638
+ #
639
+ # EQ 15kHz
640
+ #
641
+ @property
642
+ def khz15(self):
643
+ return self._khz15
644
+
645
+ @khz15.setter
646
+ def khz15(self, val: int):
647
+ self._set_frequency_on_device(self.Freqs.KHZ15, val)
648
+
649
+ @property
650
+ def khz15_db(self):
651
+ return self._map_clamp(self.khz15)
652
+
653
+ @khz15_db.setter
654
+ def khz15_db(self, val: int):
655
+ self._set_frequency_on_device_db(self.Freqs.KHZ15, val)