Passed
Push — master ( 8634b9...ba407f )
by Chris
14:07
created

MessageEncoder::encodeGroup()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
ccs 0
cts 8
cp 0
crap 6
rs 9.4285
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace DaveRandom\LibLifxLan\Encoding;
4
5
use DaveRandom\LibLifxLan\DataTypes as DeviceDataTypes;
6
use DaveRandom\LibLifxLan\DataTypes\Light as LightDataTypes;
7
use DaveRandom\LibLifxLan\Encoding\Exceptions\InvalidMessageException;
8
use DaveRandom\LibLifxLan\Messages\Device\Commands as DeviceCommands;
9
use DaveRandom\LibLifxLan\Messages\Device\Requests as DeviceRequests;
10
use DaveRandom\LibLifxLan\Messages\Device\Responses as DeviceResponses;
11
use DaveRandom\LibLifxLan\Messages\Light\Commands as LightCommands;
12
use DaveRandom\LibLifxLan\Messages\Light\Responses as LightResponses;
13
use DaveRandom\LibLifxLan\Messages\Message;
14
use const DaveRandom\LibLifxLan\FLOAT32_CODE;
15
16
final class MessageEncoder
17
{
18
    /**
19
     * @uses encodeEchoRequest
20
     * @uses encodeEchoResponse
21
     * @uses encodeSetGroup
22
     * @uses encodeStateGroup
23
     * @uses encodeStateHostFirmware
24
     * @uses encodeStateHostInfo
25
     * @uses encodeStateInfo
26
     * @uses encodeSetLabel
27
     * @uses encodeStateLabel
28
     * @uses encodeSetLocation
29
     * @uses encodeStateLocation
30
     * @uses encodeSetDevicePower
31
     * @uses encodeStateDevicePower
32
     * @uses encodeStateService
33
     * @uses encodeStateVersion
34
     * @uses encodeStateWifiFirmware
35
     * @uses encodeStateWifiInfo
36
     * @uses encodeSetColor
37
     * @uses encodeSetWaveform
38
     * @uses encodeSetWaveformOptional
39
     * @uses encodeState
40
     * @uses encodeSetInfrared
41
     * @uses encodeStateInfrared
42
     * @uses encodeSetLightPower
43
     * @uses encodeStateLightPower
44
     */
45
    private const ENCODING_ROUTINES = [
46
        // Device command messages
47
        DeviceCommands\SetGroup::class => 'SetGroup',
48
        DeviceCommands\SetLabel::class => 'SetLabel',
49
        DeviceCommands\SetLocation::class => 'SetLocation',
50
        DeviceCommands\SetPower::class => 'SetDevicePower',
51
52
        // Device request messages
53
        DeviceRequests\EchoRequest::class => 'EchoRequest',
54
55
        // Device response messages
56
        DeviceResponses\EchoResponse::class => 'EchoResponse',
57
        DeviceResponses\StateGroup::class => 'StateGroup',
58
        DeviceResponses\StateHostFirmware::class => 'StateHostFirmware',
59
        DeviceResponses\StateHostInfo::class => 'StateHostInfo',
60
        DeviceResponses\StateInfo::class => 'StateInfo',
61
        DeviceResponses\StateLabel::class => 'StateLabel',
62
        DeviceResponses\StateLocation::class => 'StateLocation',
63
        DeviceResponses\StatePower::class => 'StateDevicePower',
64
        DeviceResponses\StateService::class => 'StateService',
65
        DeviceResponses\StateVersion::class => 'StateVersion',
66
        DeviceResponses\StateWifiFirmware::class => 'StateWifiFirmware',
67
        DeviceResponses\StateWifiInfo::class => 'StateWifiInfo',
68
69
        // Light command messages
70
        LightCommands\SetColor::class => 'SetColor',
71
        LightCommands\SetInfrared::class => 'SetInfrared',
72
        LightCommands\SetPower::class => 'SetLightPower',
73
        LightCommands\SetWaveform::class => 'SetWaveform',
74
        LightCommands\SetWaveformOptional::class => 'SetWaveformOptional',
75
76
        // Light response messages
77
        LightResponses\State::class => 'State',
78
        LightResponses\StateInfrared::class => 'StateInfrared',
79
        LightResponses\StatePower::class => 'StateLightPower',
80
    ];
81
82
    private function signedShortToUnsignedShort(int $signed): int
83
    {
84
        if ($signed >= 0) {
85
            return $signed & 0x7fff;
86
        }
87
88
        return 0x8000 | (($signed & 0x7fff) + 1);
89
    }
90
91
    private function encodeHsbkColor(LightDataTypes\HsbkColor $color): string
92
    {
93
        return \pack('v4', $color->getHue(), $color->getSaturation(), $color->getBrightness(), $color->getTemperature());
94
    }
95
96
    /**
97
     * @param DeviceDataTypes\Location $location
98
     * @return string
99
     * @throws InvalidMessageException
100
     */
101
    private function encodeLocation(DeviceDataTypes\Location $location): string
102
    {
103
        $updatedAt = $this->dateTimeToNanoseconds($location->getUpdatedAt());
104
105
        if ($updatedAt < 0) {
106
            throw new InvalidMessageException("Updated at timestamp {$updatedAt} is negative");
107
        }
108
109
        return \pack('a16a32P', $location->getGuid()->getBytes(), $location->getLabel()->getValue(), $updatedAt);
110
    }
111
112
    /**
113
     * @param DeviceDataTypes\Group $group
114
     * @return string
115
     * @throws InvalidMessageException
116
     */
117
    private function encodeGroup(DeviceDataTypes\Group $group): string
118
    {
119
        $updatedAt = $this->dateTimeToNanoseconds($group->getUpdatedAt());
120
121
        if ($updatedAt < 0) {
122
            throw new InvalidMessageException("Updated at timestamp {$updatedAt} is negative");
123
        }
124
125
        return \pack('a16a32P', $group->getGuid()->getBytes(), $group->getLabel()->getValue(), $updatedAt);
126
    }
127
128
    private function encodeFirmware(DeviceDataTypes\Firmware $firmware)
129
    {
130
        return \pack(
131
            'PPV',
132
            $this->dateTimeToNanoseconds($firmware->getBuild()),
133
            0, // reserved
134
            $firmware->getVersion()
135
        );
136
    }
137
138
    private function encodeNetworkInfo(DeviceDataTypes\NetworkInfo $info)
139
    {
140
        return \pack(
141
            FLOAT32_CODE . 'VVv',
142
            $info->getSignal(),
143
            $info->getTx(),
144
            $info->getRx(),
145
            0 // reserved
146
        );
147
    }
148
149
    private function dateTimeToNanoseconds(\DateTimeInterface $dateTime): int
150
    {
151
        return ($dateTime->format('U') * 1000000000) + ($dateTime->format('u') * 1000);
152
    }
153
154
    private function encodeStateVersion(DeviceResponses\StateVersion $message): string
155
    {
156
        $version = $message->getVersion();
157
158
        return \pack('VVV', $version->getVendor(), $version->getProduct(), $version->getVersion());
159
    }
160
161
    private function encodeStateService(DeviceResponses\StateService $message): string
162
    {
163
        $service = $message->getService();
164
165
        return \pack('CV', $service->getTypeId(), $service->getPort());
166
    }
167
168
    private function encodeStateInfo(DeviceResponses\StateInfo $message): string
169
    {
170
        $info = $message->getInfo();
171
172
        return \pack('PPP', $this->dateTimeToNanoseconds($info->getTime()), $info->getUptime(), $info->getDowntime());
173
    }
174
175
    private function encodeStateHostFirmware(DeviceResponses\StateHostFirmware $message): string
176
    {
177
        return $this->encodeFirmware($message->getHostFirmware());
178
    }
179
180
    private function encodeStateHostInfo(DeviceResponses\StateHostInfo $message): string
181
    {
182
        return $this->encodeNetworkInfo($message->getHostInfo());
183
    }
184
185
    private function encodeStateWifiFirmware(DeviceResponses\StateWifiFirmware $message): string
186
    {
187
        return $this->encodeFirmware($message->getWifiFirmware());
188
    }
189
190
    private function encodeStateWifiInfo(DeviceResponses\StateWifiInfo $message): string
191
    {
192
        return $this->encodeNetworkInfo($message->getWifiInfo());
193
    }
194
195
    /**
196
     * @param DeviceCommands\SetGroup $message
197
     * @return string
198
     * @throws InvalidMessageException
199
     */
200
    private function encodeSetGroup(DeviceCommands\SetGroup $message): string
201
    {
202
        return $this->encodeGroup($message->getGroup());
203
    }
204
205
    /**
206
     * @param DeviceResponses\StateGroup $message
207
     * @return string
208
     * @throws InvalidMessageException
209
     */
210
    private function encodeStateGroup(DeviceResponses\StateGroup $message): string
211
    {
212
        return $this->encodeGroup($message->getGroup());
213
    }
214
215
    /**
216
     * @param DeviceCommands\SetLabel $message
217
     * @return string
218
     */
219
    private function encodeSetLabel(DeviceCommands\SetLabel $message): string
220
    {
221
        return \pack('a32', $message->getLabel()->getValue());
222
    }
223
224
    /**
225
     * @param DeviceResponses\StateLabel $message
226
     * @return string
227
     */
228
    private function encodeStateLabel(DeviceResponses\StateLabel $message): string
229
    {
230
        return \pack('a32', $message->getLabel()->getValue());
231
    }
232
233
    /**
234
     * @param DeviceCommands\SetLocation $message
235
     * @return string
236
     * @throws InvalidMessageException
237
     */
238
    private function encodeSetLocation(DeviceCommands\SetLocation $message): string
239
    {
240
        return $this->encodeLocation($message->getLocation());
241
    }
242
243
    /**
244
     * @param DeviceResponses\StateLocation $message
245
     * @return string
246
     * @throws InvalidMessageException
247
     */
248
    private function encodeStateLocation(DeviceResponses\StateLocation $message): string
249
    {
250
        return $this->encodeLocation($message->getLocation());
251
    }
252
253
    /**
254
     * @param DeviceCommands\SetPower $message
255
     * @return string
256
     */
257
    private function encodeSetDevicePower(DeviceCommands\SetPower $message): string
258
    {
259
        return \pack('v', $message->getLevel());
260
    }
261
262
    /**
263
     * @param DeviceResponses\StatePower $message
264
     * @return string
265
     */
266
    private function encodeStateDevicePower(DeviceResponses\StatePower $message): string
267
    {
268
        return \pack('v', $message->getLevel());
269
    }
270
271
    /**
272
     * @param DeviceRequests\EchoRequest $message
273
     * @return string
274
     * @throws InvalidMessageException
275
     */
276
    private function encodeEchoRequest(DeviceRequests\EchoRequest $message): string
277
    {
278
        $payload = $message->getPayload();
279
280
        if (\strlen($payload) !== 64) {
281
            throw new InvalidMessageException("Echo request payload should be exactly 64 bytes");
282
        }
283
284
        return $payload;
285
    }
286
287
    /**
288
     * @param DeviceResponses\EchoResponse $message
289
     * @return string
290
     */
291
    private function encodeEchoResponse(DeviceResponses\EchoResponse $message): string
292
    {
293
        return $message->getPayload(); // don't validate this as it should respond with client data verbatim
294
    }
295
296
    /**
297
     * @param LightCommands\SetColor $message
298
     * @return string
299
     */
300
    private function encodeSetColor(LightCommands\SetColor $message): string
301
    {
302
        $transition = $message->getColorTransition();
303
304
        return "\x00" . $this->encodeHsbkColor($transition->getColor()) . \pack('V', $transition->getDuration());
305
    }
306
307
    /**
308
     * @param LightCommands\SetInfrared $message
309
     * @return string
310
     */
311
    private function encodeSetInfrared(LightCommands\SetInfrared $message): string
312
    {
313
        return \pack('v', $message->getBrightness());
314
    }
315
316
    /**
317
     * @param LightResponses\StateInfrared $message
318
     * @return string
319
     */
320
    private function encodeStateInfrared(LightResponses\StateInfrared $message): string
321
    {
322
        return \pack('v', $message->getBrightness());
323
    }
324
325
    /**
326
     * @param LightCommands\SetPower $message
327
     * @return string
328
     */
329
    private function encodeSetLightPower(LightCommands\SetPower $message): string
330
    {
331
        $transition = $message->getPowerTransition();
332
333
        return \pack('vV', $transition->getLevel(), $transition->getDuration());
334
    }
335
336
    /**
337
     * @param LightResponses\StatePower $message
338
     * @return string
339
     */
340
    private function encodeStateLightPower(LightResponses\StatePower $message): string
341
    {
342
        return \pack('v', $message->getLevel());
343
    }
344
345
    /**
346
     * @param LightResponses\State $message
347
     * @return string
348
     */
349
    private function encodeState(LightResponses\State $message): string
350
    {
351
        $state = $message->getState();
352
353
        return $this->encodeHsbkColor($state->getColor()) . \pack(
354
            'v2a32P',
355
            0, // reserved
356
            $state->getPower(),
357
            $state->getLabel()->getValue(),
358
            0  // reserved
359
        );
360
    }
361
362
    /**
363
     * @param LightCommands\SetWaveform $message
364
     * @return string
365
     */
366
    private function encodeSetWaveform(LightCommands\SetWaveform $message): string
367
    {
368
        $effect = $message->getEffect();
369
        $skew = $this->signedShortToUnsignedShort($effect->getSkewRatio());
370
371
        return "\x00" . \chr((int)$effect->isTransient())
372
            . $this->encodeHsbkColor($effect->getColor())
373
            . \pack('V' . FLOAT32_CODE . 'vC', $effect->getPeriod(), $effect->getCycles(), $skew, $effect->getWaveform())
374
        ;
375
    }
376
377
    /**
378
     * @param LightCommands\SetWaveformOptional $message
379
     * @return string
380
     */
381
    private function encodeSetWaveformOptional(LightCommands\SetWaveformOptional $message): string
382
    {
383
        $effect = $message->getEffect();
384
        $skew = $this->signedShortToUnsignedShort($effect->getSkewRatio());
385
386
        $options = $effect->getOptions();
387
        $optionData = \pack(
388
            'C4',
389
            (int)(bool)($options & LightDataTypes\Effect::SET_HUE),
390
            (int)(bool)($options & LightDataTypes\Effect::SET_SATURATION),
391
            (int)(bool)($options & LightDataTypes\Effect::SET_BRIGHTNESS),
392
            (int)(bool)($options & LightDataTypes\Effect::SET_TEMPERATURE)
393
        );
394
395
        return "\x00" . \chr((int)$effect->isTransient())
396
            . $this->encodeHsbkColor($effect->getColor())
397
            . \pack('V' . FLOAT32_CODE . 'vC', $effect->getPeriod(), $effect->getCycles(), $skew, $effect->getWaveform())
398
            . $optionData
399
        ;
400
    }
401
402
    public function encodeMessage(Message $message): string
403
    {
404
        return \array_key_exists($class = \get_class($message), self::ENCODING_ROUTINES)
405
            ? $this->{'encode' . self::ENCODING_ROUTINES[$class]}($message)
406
            : '';
407
    }
408
}
409