├── .gitignore ├── API.md ├── LICENSE ├── README.md ├── TODO.md ├── expected_settings_backup_for_integration_test_hw_v2.json ├── expected_settings_backup_for_integration_test_hw_v3.json ├── integration-test.sh ├── requirements.txt ├── sem6000-cli-demo.py ├── sem6000-read-tests.py ├── sem6000-settings-backup-demo.py ├── sem6000-settings-restore-demo.py ├── sem6000 ├── __init__.py ├── bluetooth_lowenergy_interface │ ├── abstract_interface.py │ ├── bluepy_interface.py │ └── timeout_decorator.py ├── encoder.py ├── message.py ├── parser.py ├── repeat_on_failure_decorator.py ├── sem6000.py ├── tests │ ├── __init__.py │ └── test_message_parser_and_encoder.py └── util.py └── settings_for_integration_test.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | original_settings.json 3 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # Characteristics 2 | 3 | In order to control the device you need to determine the correct handles by quering the characteristics, e.g. 4 | 5 | ``` 6 | $ gatttool -b FC:69:47:06:CB:C6 -I 7 | [FC:69:47:06:CB:C6][LE]> connect 8 | Attempting to connect to FC:69:47:06:CB:C6 9 | Connection successful 10 | [FC:69:47:06:CB:C6][LE]> characteristics 11 | ... 12 | handle: 0x0002, char properties: 0x02, char value handle: 0x0003, uuid: 00002a00-0000-1000-8000-00805f9b34fb 13 | ... 14 | handle: 0x0024, char properties: 0x06, char value handle: 0x0025, uuid: 0000fff1-0000-1000-8000-00805f9b34fb 15 | handle: 0x0027, char properties: 0x02, char value handle: 0x0028, uuid: 0000fff2-0000-1000-8000-00805f9b34fb 16 | handle: 0x002a, char properties: 0x04, char value handle: 0x002b, uuid: 0000fff3-0000-1000-8000-00805f9b34fb 17 | handle: 0x002d, char properties: 0x10, char value handle: 0x002e, uuid: 0000fff4-0000-1000-8000-00805f9b34fb 18 | ... 19 | ``` 20 | | uuid | handle | description | 21 | |------|--------|--------------------------------------------------| 22 | | 2a00 | 0x0003 | Device name | 23 | | fff1 | 0x0025 | Device vendor, hardware, firmware | 24 | | fff3 | 0x002b | Handle for command requests ```char-write-cmd``` | 25 | | fff4 | 0x002e | Handle for receiving notifications | 26 | 27 | Note: The handles can be different. They relate on firmware and hardware version of your device 28 | 29 | # Device vendor, hardware, firmware 30 | 31 | ``` 32 | char-read-hnd 0025 33 | 34 | Characteristic value/descriptor: 56 4f 4c 43 46 54 04 00 00 00 00 01 0d 02 00 1e 35 | | | + Hardware version (2 bytes for major and minor), here: 2.0 36 | | + Firmware version (2 bytes for major and minor), here: 1.13 37 | + Vendor string in ASCII (6 bytes) 38 | ``` 39 | 40 | # Hardware version and MTU 41 | If you have a device with hardware version 2 or lower you will get multiple notifications per request which depends on the type of request. 42 | If you have a device with hardware version 3 or above you MUST set the so called MTU so that the package size of notifications is set to 160 bytes. Then you get always just one notification per request. 43 | 44 | ``` 45 | mtu 160 46 | MTU was exchanged successfully: 160 47 | ``` 48 | 49 | # PIN 50 | ## Authorization with PIN 51 | ``` 52 | char-write-cmd 0x2b 0f0c170000000000000000000018ffff 53 | | | | | | | | | + static end sequence of message, 0xffff 54 | | | | | | | | + checksum byte starting with length-byte, ending w/ byte before 55 | | | | | | | + always 0x00000000 56 | | | | | + PIN, 4 bytes e.g. 01020304 57 | | | | + 0x00 for authorization request 58 | | | + Authorization command 0x1700 59 | | + Length of payload starting w/ next byte incl. checksum 60 | + static start sequence for message, 0x0f 61 | 62 | 63 | Notification handle = 0x002e value: 0f 06 17 00 00 00 00 18 ff ff 64 | | | | | | | + static end sequence of message, 0xffff 65 | | | | | | + checksum byte starting with length-byte, ending w/ byte before 66 | | | | | + always 0x0000 67 | | | | + 0 = success, 1 = unsuccess 68 | | | + Login command 0x1700 69 | | + Length of payload starting w/ next byte incl. checksum 70 | + static start sequence for message, 0x0f 71 | ``` 72 | 73 | 74 | ## Change PIN 75 | ``` 76 | char-write-cmd 0x2b 0f0c170001010203040000000018ffff 77 | | | | | | | + static end sequence of message, 0xffff 78 | | | | | | + checksum byte starting with length-byte, ending w/ byte before 79 | | | | | + old PIN, 4 bytes e.g. 01020304 80 | | | | + new PIN, 4 bytes e.g. 01020304 81 | | | + Change pin command 0x170001 82 | | + Length of payload starting w/ next byte incl. checksum 83 | + static start sequence for message, 0x0f 84 | 85 | 86 | Notification handle = 0x002e value: 0f 06 17 00 00 01 00 18 ff ff 87 | | | | | | + static end sequence of message, 0xffff 88 | | | | | + checksum byte starting with length-byte, ending w/ byte before 89 | | | | + 0 = success 90 | | | + Login command 0x170000 91 | | + Length of payload starting w/ next byte incl. checksum 92 | + static start sequence for message, 0x0f 93 | ``` 94 | 95 | ## Reset PIN to "0000" 96 | ``` 97 | char-write-cmd 0x2b 0f0c170002000000000000000018ffff 98 | | | | | | | + static end sequence of message, 0xffff 99 | | | | | | + checksum byte starting with length-byte, ending w/ byte before 100 | | | | | + static 0x0000000000000000 101 | | | | + 0x02 for reset PIN 102 | | | + Authorization command 0x1700 103 | | + Length of payload starting w/ next byte incl. checksum 104 | + static start sequence for message, 0x0f 105 | 106 | 107 | Notification handle = 0x002e value: 0f 06 17 00 00 02 00 18 ff ff 108 | | | | | | + static end sequence of message, 0xffff 109 | | | | | + checksum byte starting with length-byte, ending w/ byte before 110 | | | | + 0 = success 111 | | | + Login command 0x170000 112 | | + Length of payload starting w/ next byte incl. checksum 113 | + static start sequence for message, 0x0f 114 | ``` 115 | 116 | # Setup and settings 117 | ## Synchronize datetime 118 | ``` 119 | char-write-cmd 0x2b 0f0c010029180a160607e3000053ffff 120 | | | | | | | | | | | | | + always 0xffff 121 | | | | | | | | | | | | + checksum byte starting with length-byte 122 | | | | | | | | | | | + always 0000 123 | | | | | | | | | +-+ year, high-byte, low-byte 124 | | | | | | | | + month 125 | | | | | | | + day of month 126 | | | | | | + hour 127 | | | | | + minute 128 | | | | + Seconds 129 | | | + Set datetime command, 0x0100 130 | | + Length of payload starting w/ next byte incl. checksum 131 | + static start sequence for message, 0x0f 132 | 133 | Notification handle = 0x002e value: 0f 04 01 00 00 02 ff ff 134 | | | | | | | + static end sequence of message, 0xffff 135 | | | | | + checksum byte starting with length-byte, ending w/ byte before 136 | | | | + Success 137 | | | + Set datetime command, 0x0100 138 | | + Length of payload starting w/ next byte incl. checksum 139 | + static start sequence for message, 0x0f 140 | ``` 141 | 142 | ## Request settings 143 | ``` 144 | char-write-cmd 0x2b 0f051000000011ffff 145 | | | | | | + always ffff 146 | | | | | + checksum byte starting with length-byte 147 | | | | + always 0000 148 | | | + Request settings command, 0x1000 149 | | + Length of payload starting w/ next byte incl. checksum 150 | + static start sequence for message, 0x0f 151 | 152 | Notification handle = 0x002e value: 0f 0e 10 00 00 c8 64 00 00 00 00 01 00 0e 60 ac ff ff 153 | | | | | | | | | | | | | | | + static end sequence of message, 0xffff 154 | | | | | | | | | | | | | | + checksum byte starting with length-byte, ending w/ byte before 155 | | | | | | | | | | | | | + Over power low-byte 156 | | | | | | | | | | | | + Over power high-byte 157 | | | | | | | | | | | + LED 1=on / 0=off 158 | | | | | | | | | | + reduced mode end in min lox-byte 159 | | | | | | | | | + reduced mode end in min high-byte 160 | | | | | | | | + reduced mode start in min low-byte 161 | | | | | | | + reduced mode start in min high-byte 162 | | | | | | + reduced price / 100.0 163 | | | | | + normal Price / 100.0 164 | | | | + reduced mode active 1=yes, 0=no 165 | | | + Request settings command, 0x1000 166 | | + Length of payload starting w/ next byte incl. checksum 167 | + static start sequence for message, 0x0f 168 | ``` 169 | 170 | ## Set LED ring 171 | ``` 172 | char-write-cmd 0x2b 0f090f0005010000000016ffff 173 | | | | | | | | + always 0xffff 174 | | | | | | | + checksum byte starting with length-byte 175 | | | | | | + always 0x00000000 176 | | | | | + 1 = on, 0 = off 177 | | | | + always 0x05 178 | | | + Set LED ring command, 0x0f00 179 | | + Length of payload starting w/ next byte incl. checksum 180 | + static start sequence for message, 0x0f 181 | 182 | Notification handle = 0x002e value: 0f 05 0f 00 05 00 15 ff ff 183 | | | | | | + static end sequence of message, 0xffff 184 | | | | | + checksum byte starting with length-byte, ending w/ byte before 185 | | | | + always 0x0500 186 | | | + Set LED ring command, 0x0f00 187 | | + Length of payload starting w/ next byte incl. checksum 188 | + static start sequence for message, 0x0f 189 | ``` 190 | 191 | ## Set overload power 192 | ``` 193 | char-write-cmd 2b 0f0705000e60000074ffff 194 | | | | | | | + static end sequence of message, 0xffff 195 | | | | | | + checksum byte starting with length-byte, ending w/ byte before 196 | | | | | + status 0x0000 197 | | | | + overload value, low-byte 198 | | | | + overload value, high-byte 199 | | | + Set overload command, 0x0500 200 | | + Length of payload starting w/ next byte incl. checksum 201 | + static start sequence for message, 0x0f 202 | 203 | Notification handle = 0x002e value: 0f 04 05 00 00 06 ff ff 204 | | | | | | + static end sequence of message, 0xffff 205 | | | | | + checksum byte starting with length-byte, ending w/ byte before 206 | | | | + always 0x00 207 | | | + Set overload command, 0x0500 208 | | + Length of payload starting w/ next byte incl. checksum 209 | + static start sequence for message, 0x0f 210 | ``` 211 | 212 | ## Set prices 213 | ``` 214 | char-write-cmd 2b 0f90f00047b2d00000000b2ffff 215 | | | | | | | | + End sequence 0xffff 216 | | | | | | | + Checksum 217 | | | | | | + static 0x00000000 218 | | | | | + reduced price * 100 219 | | | | + normal price * 100 220 | | | + Setprice command, 0x0f0004 221 | | + Length of payload starting w/ next byte incl. checksum 222 | + static start sequence for message, 0x0f 223 | 224 | Notification handle = 0x2b value: 0f 05 0f 00 04 00 14 ff ff 225 | | | | | | + static end sequence of message, 0xffff 226 | | | | | + checksum byte starting with length-byte, ending w/ byte before 227 | | | | + always 0x00 228 | | | + Set price command, 0x0f0004 229 | | + Length of payload starting w/ next byte incl. checksum 230 | + static start sequence for message, 0x0f 231 | ``` 232 | 233 | ## Set reduced period 234 | ``` 235 | char-write-cmd 2b 0f090f000101005301288effff 236 | | | | | | | | + End sequence 0xffff 237 | | | | | | | + Checksum 238 | | | | | | + 2 bytes for end time in minutes, here 04:56 239 | | | | | + 2 bytes for start time in minutes, here 01:23 240 | | | | + reduced period on / off, 1 = on, 0 = off 241 | | | + Set reduced period command, 0x0f0001 242 | | + Length of payload starting w/ next byte incl. checksum 243 | + static start sequence for message, 0x0f 244 | 245 | Notification handle = 0x2b value: 0f 05 0f 00 01 00 11 ff ff 246 | | | | | | + static end sequence of message, 0xffff 247 | | | | | + checksum byte starting with length-byte, ending w/ byte before 248 | | | | + always 0x00 249 | | | + Set reduced period command, 0x0f0001 250 | | + Length of payload starting w/ next byte incl. checksum 251 | + static start sequence for message, 0x0f 252 | ``` 253 | 254 | # Switch on / off 255 | ``` 256 | char-write-cmd 0x2b 0f06030000000004ffff 257 | | | | | | | + static end sequence of message, 0xffff 258 | | | | | | + checksum byte starting with length-byte, ending w/ byte before 259 | | | | | + Static 0x0000 260 | | | | + 0x01 = turn on, 0x00 = turn off 261 | | | + Switch command 0x0300 262 | | + Length of payload starting w/ next byte incl. checksum 263 | + static start sequence for message, 0x0f 264 | 265 | 266 | Notification handle = 0x002e value: 0f 04 03 00 00 04 ff ff 267 | | | | | | + static end sequence of message, 0xffff 268 | | | | | + checksum byte starting with length-byte, ending w/ byte before 269 | | | | + 0 = success 270 | | | + Switch command 0x0300 271 | | + Length of payload starting w/ next byte incl. checksum 272 | + static start sequence for message, 0x0f 273 | ``` 274 | 275 | # Timer 276 | ## Get timer status 277 | ``` 278 | char-write-cmd 2b 0f05090000000affff 279 | 280 | Notification handle = 0x2b value: 0f 0e 09 00 01 10 04 10 08 07 13 01 51 45 00 e8 ff ff 281 | | | | | | | | | | | | | + static end sequence of message, 0xffff 282 | | | | | | | | | | | | + checksum byte starting with length-byte, ending w/ byte before 283 | | | | | | | | | | | + Origin runtime in seconds, 3 bytes 284 | | | | | | | | | | + target year 285 | | | | | | | | | + target Month 286 | | | | | | | | + target day on month 287 | | | | | | | + target hour 288 | | | | | | + target minute 289 | | | | | + target second 290 | | | | + Action, 1 = turn on, 2 = turn off 291 | | | + Request timer status command, 0x0900 292 | | + Length of payload starting w/ next byte incl. checksum 293 | + static start sequence for message, 0x0f 294 | ``` 295 | 296 | ## Set timer 297 | ``` 298 | char-write-cmd 2b 0f0c0800012d1c1607071300008affff 299 | | | | | | | | | | | | | + static end sequence of message, 0xffff 300 | | | | | | | | | | | | + checksum byte starting with length-byte, ending w/ byte before 301 | | | | | | | | | | | + Static 0x0000 302 | | | | | | | | | | + Schedule year 303 | | | | | | | | | + Schedule month 304 | | | | | | | | + Schedule day of month 305 | | | | | | | + Schedule hour 306 | | | | | | + Schedule minutes 307 | | | | | + Schedule seconds 308 | | | | + Timer action, 1 = on, 2 = off 309 | | | + Set timer command, 0x0800 310 | | + Length of payload starting w/ next byte incl. checksum 311 | + static start sequence for message, 0x0f 312 | 313 | Notification handle = 0x2b value: 0f 04 08 00 00 09 ff ff 314 | | | | | + static end sequence of message, 0xffff 315 | | | | + checksum byte starting with length-byte, ending w/ byte before 316 | | | + Request timer status command, 0x080000 317 | | + Length of payload starting w/ next byte incl. checksum 318 | + static start sequence for message, 0x0f 319 | ``` 320 | 321 | ## Stop timer 322 | ``` 323 | char-write-cmd 2b 0f0c080000000000000000000009ffff 324 | | | | | | | | | | | | | + static end sequence of message, 0xffff 325 | | | | | | | | | | | | + checksum byte starting with length-byte, ending w/ byte before 326 | | | | | | | | | | | + Static 0x0000 327 | | | | | | | | | | + Schedule year, 0 for reset 328 | | | | | | | | | + Schedule month, 0 for reset 329 | | | | | | | | + Schedule day of month, 0 for reset 330 | | | | | | | + Schedule hour, 0 for reset 331 | | | | | | + Schedule minutes, 0 for reset 332 | | | | | + Schedule seconds, 0 for reset 333 | | | | + Timer action, 0 = reset 334 | | | + Set timer command, 0x0800 335 | | + Length of payload starting w/ next byte incl. checksum 336 | + static start sequence for message, 0x0f 337 | 338 | Notification handle = 0x2e value: 0f 04 08 00 00 09 ff ff 339 | | | | | + static end sequence of message, 0xffff 340 | | | | + checksum byte starting with length-byte, ending w/ byte before 341 | | | + Request timer status command, 0x080000 342 | | + Length of payload starting w/ next byte incl. checksum 343 | + static start sequence for message, 0x0f 344 | ``` 345 | 346 | # Scheduler 347 | ## Request scheduler 348 | 349 | No schedulers set 350 | ``` 351 | char-write-cmd 2b 0f06140000000015ffff 352 | | | | | | + static end sequence of message, 0xffff 353 | | | | | + checksum byte starting with length-byte, ending w/ byte before 354 | | | | + Page if more than 4 schedulers, 0 = 1st page , 1 = 2nd page 355 | | | + Request schedulers command, 0x1400 356 | | + Length of payload starting w/ next byte incl. checksum 357 | + static start sequence for message, 0x0f 358 | 359 | No schedulers: 360 | Notification handle = 0x2e value: 0f 04 14 00 00 15 ff ff 361 | | | | + Number of active schedulers, here no slots set 362 | | | + Request schedulers command, 0x1400 363 | | + Length of payload starting w/ next byte incl. checksum 364 | + static start sequence for message, 0x0f 365 | ``` 366 | 367 | If only 1 scheduler is set then there is a single notification. 368 | ``` 369 | Notification handle = 0x2e value: 0f 10 14 00 01 0c 01 01 00 13 08 09 0a 0b 00 00 4f ac ff ff 370 | | | | | | | | | | | | | | | + some kind of checksum for specific scheduler 371 | | | | | | | | | | | | | | + static, 0x0000 372 | | | | | | | | | | | | | + Minute 373 | | | | | | | | | | | | + Hour 374 | | | | | | | | | | | + day in month 375 | | | | | | | | | | + Month 376 | | | | | | | | | + Year (2 digts) 377 | | | | | | | | + Weekday mask, 0 if once 378 | | | | | | | + Action, 1 = turn on, 0 = turn off 379 | | | | | | + Active, 0 = inactive, 1 = active 380 | | | | | + Slot ID 381 | | | | + Number of active schedulers, here 1 382 | | | + Request schedulers command, 0x1400 383 | | + Length of payload starting w/ next byte incl. checksum 384 | + static start sequence for message, 0x0f 385 | ``` 386 | 387 | If there are more than 1 schedulers set then there are multiple notifications. 388 | ``` 389 | # notification #1 390 | Notification handle = 0x2e value: 0f 28 14 00 03 0a 01 01 01 13 07 0d 0b 2c 00 00 75 0b 01 00 391 | | | | | | | | | | | | | | | + Checksum of scheduler? 392 | | | | | | | | | | | | | | + static, 0x0000 393 | | | | | | | | | | | | | + Minute 394 | | | | | | | | | | | | + Hour 395 | | | | | | | | | | | + day in month 396 | | | | | | | | | | + Month 397 | | | | | | | | | + Year (2 digts) 398 | | | | | | | | + Weekday mask, 0 if once, 1 = Sunday, 2 = Monday, 4 = Tuesday, etc. 399 | | | | | | | + Action, 1 = turn on, 0 = turn off 400 | | | | | | + Active, 0 = inactive, 1 = active 401 | | | | | + Slot ID, starting with 10, 0x0a=0, 0x0b=1, 0x0c=2 402 | | | | + Number of active schedulers, here 3 403 | | | + Request schedulers command, 0x1400 404 | | + Length of payload starting w/ next byte incl. checksum 405 | + static start sequence for message, 0x0f 406 | 407 | # notification #2 408 | Notification handle = 0x2e value: 7f 13 07 0d 0e 0f 00 00 e4 0c 00 01 00 13 08 09 0a 0b 00 00 409 | 410 | # notification #3 411 | Notification handle = 0x2e value: 5b 4c ff ff 412 | ``` 413 | 414 | Request 2nd page 415 | ``` 416 | char-write-cmd 2b 0f06140001000016ffff 417 | + Page if more than 4 schedulers, 0 = 1st page , 1 = 2nd page 418 | 419 | Notification handle = 0x2e value: 0f 10 14 00 05 0b 01 00 01 13 07 0e 01 01 00 00 40 91 ff ff 420 | ``` 421 | 422 | ## Set scheduler 423 | ``` 424 | char-write-cmd 2b 0f0f1300010001010113070e0e1a000068ffff 425 | | | | | | | | | | | | | | | | + Static end sequence of message, 0xffff 426 | | | | | | | | | | | | | | | + Checksum byte starting with length-byte, ending w/ byte before 427 | | | | | | | | | | | | | | + Static 0x0000 428 | | | | | | | | | | | | | + Minute 429 | | | | | | | | | | | | + Hour 430 | | | | | | | | | | | + Day of month 431 | | | | | | | | | | + Month, 1 = January 432 | | | | | | | | | + Year, 2 digits, eg. 19 for 2019 433 | | | | | | | | + Weekday mask, 7 bits, 1st bit = Sunday, ... 434 | | | | | | | + Action, 1 = turn on, 0 = turn off 435 | | | | | | + State , 1 = active, 0 = inactive 436 | | | | | + If edit / remote scheduler then ID of slot else 0x00 437 | | | | + 0 = add new scheduler, 1 = edit existing scheduler, 2 = remove scheduler 438 | | | + Set scheduler command, 0x1300 439 | | + Length of payload starting w/ next byte incl. checksum 440 | + Static start sequence for message, 0x0f 441 | 442 | Notification handle = 0x2b value: 0f 06 13 00 01 00 00 15 ff ff 443 | | | | | | | + Static end sequence of message, 0xff 444 | | | | | | + Checksum byte starting with length-byte, ending w/ byte before 445 | | | | | + Static 0x0000 446 | | | | + 0 = success, 1 = unsuccess 447 | | | + Set scheduler command, 0x1300 448 | | + Length of payload starting w/ next byte incl. checksum 449 | + Static start sequence for message, 0x0f 450 | ``` 451 | 452 | ## Reset scheduler 453 | ``` 454 | char-write-cmd 2b 0f0f1300020c0000000000000000000022ffff 455 | | + ID of slot 456 | + 2 = remove scheduler 457 | ``` 458 | 459 | # Random mode 460 | ## Get randommode 461 | 462 | ``` 463 | char-write-cmd 0x2b 0f051600000017ffff 464 | 465 | Notification handle = 0x2b value: 0f 0b 16 00 01 55 02 03 04 05 00 00 7b ff ff 466 | | | | | | | | | | | + Static end sequence of message, 0xffff 467 | | | | | | | | | | + Checksum byte starting with length-byte, ending w/ byte before 468 | | | | | | | | | + End minute 469 | | | | | | | | + End hour 470 | | | | | | | + Start minute 471 | | | | | | + Start hour 472 | | | | | + Weekday mask, bit 1 = Sunday, bit 2 = Monday, etc. 473 | | | | + Randommode status, 1 = on, 0 = off 474 | | | + Request random mode status command, 0x1600 475 | | + Length of payload starting w/ next byte incl. checksum 476 | + static start sequence for message, 0x0f 477 | ``` 478 | 479 | ## Set random mode 480 | ``` 481 | char-write-cmd 2b 0f0b1500017f020304050000a4ffff 482 | | | | | | | | | | | + Static end sequence of message, 0xffff 483 | | | | | | | | | | + Checksum byte starting with length-byte, ending w/ byte before 484 | | | | | | | | | + End minute 485 | | | | | | | | + End hour 486 | | | | | | | + Start minute 487 | | | | | | + Start hour 488 | | | | | + Weekday mask, bit 1 = Sunday, bit 2 = Monday, etc. 489 | | | | + Randommode status, 1 = on, 0 = off 490 | | | + Set randommode command 0x1500 491 | | + Length of payload starting w/ next byte incl. checksum 492 | + static start sequence for message, 0x0f 493 | 494 | Notification handle = 0x2b value: 0f 04 15 00 00 16 ff ff 495 | ``` 496 | 497 | # Measure power and consumption 498 | ## Capture measurement 499 | ``` 500 | char-write-cmd 0x2b 0f050400000005ffff 501 | | | | | + static end sequence of message, 0xffff 502 | | | | + checksum byte starting with length-byte, ending w/ byte before 503 | | | + Capture measurement command 0x040000 504 | | + Length of payload starting w/ next byte incl. checksum 505 | + static start sequence for message, 0x0f 506 | 507 | 508 | Notification handle = 0x002e value: 0f 11 04 00 01 00 00 00 eb 00 0c 32 00 00 00 00 00 00 2f 509 | | | | | | | | | | | + Static end sequence (0xffff) is missing in this notification! 510 | | | | | | | | | | + checksum byte starting with length-byte 511 | | | | | | | | | + total consumption, 4 bytes (always 0 for hardware versions < 3) 512 | | | | | | | | + frequency (Hz) 513 | | | | | | | + Ampere/1000 (A), 2 bytes 514 | | | | | | + Voltage (V) 515 | | | | | | 516 | | | | | + Watt/1000, 3 bytes 517 | | | | + Power, 0 = off, 1 = on 518 | | | + Capture measurement response 0x0400 519 | | + Length of payload starting w/ next byte incl. checksum 520 | + static start sequence for message, 0x0f 521 | ``` 522 | 523 | Note: Typical 0xffff end sequence is missing in this response. This is probably since there is no room for it. 524 | 525 | Starting with hardware version 3 the reported length is shorter, i.e. ```0x0f``` instead of ```0xff```. 526 | 527 | A notification looks like this: 528 | ``` 529 | +- Length of payload starting w/ next byte. -+ 530 | | | 531 | Notification handle = 0x0014 value: 0f 0f 04 00 01 00 88 50 dc 00 d6 32 01 00 00 00 00 67 2a 532 | | | | | | | | | | + Static end sequence (0xffff) is missing in this notification! 533 | | | | | | | | | + checksum byte starting with length-byte 534 | | | | | | | | | + total consumption, 4 bytes (hardware versions >= 3 there is a value) 535 | | | | | | | + frequency (Hz) 536 | | | | | | + Ampere/1000 (A), 2 bytes 537 | | | | | + Voltage (V) 538 | | | | | 539 | | | | + Watt/1000, 3 bytes 540 | | | + Power, 0 = off, 1 = on 541 | | + Capture measurement response 0x0400 542 | + static start sequence for message, 0x0f 543 | ``` 544 | 545 | 546 | 547 | ## Reguest measurements for year, month and day 548 | ``` 549 | char-write-cmd 2b 0f050b0000000cffff 550 | | | | | | + static end sequence of message, 0xffff 551 | | | | | + checksum byte starting with length-byte, ending w/ byte before 552 | | | | + Static 0x000000 553 | | | + 0a = last 24h per hour, 0b = last 30 days per day, 0c = last year per month 554 | | + Length of payload starting w/ next byte incl. checksum 555 | + static start sequence for message, 0x0f 556 | 557 | ``` 558 | 559 | After this request there are several notification handles. 560 | 561 | ### Year 562 | For requests on year level there are 12 records for each month. Each records has 4 bytes and 3 bytes representing for consumption in Wh. 563 | 564 | ``` 565 | Notification handle = 0x002e value: 0f 33 0c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 566 | | | | | | | + Current month - 8, 3 bytes for Wh 567 | | | | | | + Current month - 9, 3 bytes for Wh 568 | | | | | + Current month - 10, 3 bytes for Wh 569 | | | | + Current month - 11, 3 bytes for Wh 570 | | | + 0x0c00, Request data for year request 571 | | + Length of payload starting w/ next byte incl. checksum 572 | + static start sequence for message, 0x0f 573 | 574 | Notification handle = 0x002e value: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 575 | | | | | + Current month - 3, 3 bytes for Wh 576 | | | | + Current month - 4, 3 bytes for Wh 577 | | | + Current month - 5, 3 bytes for Wh 578 | | + Current month - 6, 3 bytes for Wh 579 | + Current month - 7, 3 bytes for Wh 580 | 581 | Notification handle = 0x002e value: 00 00 00 00 00 00 00 00 00 04 e3 00 f4 ff ff 582 | | | | | + static end sequence of message, 0xffff 583 | | | | + checksum byte starting with length-byte, ending w/ byte before 584 | | | + Current month, 3 bytes for Wh 585 | | + Current month - 1, 3 bytes for Wh 586 | + Current month - 2, 3 bytes for Wh 587 | ``` 588 | 589 | ### Month 590 | For requests on month level there are 30 records for each day in month. Each records has 4 bytes and 3 bytes representing for consumption in Wh. 591 | 592 | ``` 593 | Notification handle = 0x002e value: 0f 7b 0b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 594 | | | | | | | + Today - 26, 3 bytes for Wh 595 | | | | | | + Today - 27, 3 bytes for Wh 596 | | | | | + Today - 28, 3 bytes for Wh 597 | | | | + Today - 29, 3 bytes for Wh 598 | | | + 0x0b00, Request data for month request 599 | | + Length of payload starting w/ next byte incl. checksum 600 | + static start sequence for message, 0x0f 601 | 602 | Notification handle = 0x002e value: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 603 | Notification handle = 0x002e value: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 604 | Notification handle = 0x002e value: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 605 | Notification handle = 0x002e value: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 606 | Notification handle = 0x002e value: 00 00 00 00 00 00 e3 00 00 01 37 00 00 01 23 00 00 01 37 00 607 | Notification handle = 0x002e value: 00 00 6f 00 f2 ff ff 608 | | | + static end sequence of message, 0xffff 609 | | + checksum byte starting with length-byte, ending w/ byte before 610 | + Today, 3 bytes for Wh 611 | ``` 612 | 613 | ### Day 614 | For requests on day level there are 24 records for each hour in day. Each records has 2 bytes representing for consumption in Wh. 615 | 616 | ``` 617 | Notification handle = 0x002e value: 0f 33 0a 00 00 0e 00 0e 00 0e 00 0e 00 0c 00 09 00 08 00 0b 618 | | | | | | | | | | | + Current hour - 16, 2 bytes for Wh 619 | | | | | | | | | | + Current hour - 17, 2 bytes for Wh 620 | | | | | | | | | + Current hour - 18, 2 bytes for Wh 621 | | | | | | | | + Current hour - 19, 2 bytes for Wh 622 | | | | | | | + Current hour - 20, 2 bytes for Wh 623 | | | | | | + Current hour - 21, 2 bytes for Wh 624 | | | | | + Current hout - 22, 3 bytes for Wh 625 | | | | + Current hour - 23, 2 bytes for Wh 626 | | | + 0x0a00, Request data for day request 627 | | + Length of payload starting w/ next byte incl. checksum 628 | + static start sequence for message, 0x0f 629 | 630 | Notification handle = 0x002e value: 00 0e 00 0e 00 11 00 0f 00 10 00 0f 00 0d 00 0e 00 0e 00 0e 631 | Notification handle = 0x002e value: 00 0e 00 0e 00 0e 00 0e 00 0d 00 00 42 ff ff 632 | | | | + static end sequence of message, 0xffff 633 | | | + checksum byte starting with length-byte, ending w/ byte before 634 | | + Current hour, 2 bytes for Wh 635 | + Current hour - 2 , 2 bytes for Wh 636 | ``` 637 | 638 | ## Reset data 639 | ``` 640 | char-write-cmd 2b 0f090f0000000000000010ffff 641 | | | | | | + static end sequence of message, 0xffff 642 | | | | | + checksum byte starting with length-byte, ending w/ byte before 643 | | | | + 0 = factory reset, 2 = reset stored consumption 644 | | | + Reset command 0x0f00 645 | | + Length of payload starting w/ next byte incl. checksum 646 | + static start sequence for message, 0x0f 647 | 648 | 649 | Notification handle = 0x002b value: 0f 05 0f 00 00 00 10 ff ff 650 | | | | | | + Static end sequence (0xffff) is missing in this notification! 651 | | | | | + checksum byte starting with length-byte 652 | | | | + 0 = factory reset, 2 = reset stored consumption 653 | | | + Reset command, 0x0f00 654 | | + Length of payload starting w/ next byte incl. checksum 655 | + static start sequence for message, 0x0f 656 | ``` 657 | 658 | 659 | # Device settings 660 | ## Get name 661 | ``` 662 | char-read-hnd 3 663 | Characteristic value/descriptor: 48 6f 6c 6c 61 64 69 65 77 61 6c 64 66 65 65 664 | 665 | Convert values to ascii,e.g. 666 | 667 | Holladiewaldfee 668 | ``` 669 | 670 | ## Set name 671 | ``` 672 | char-write-cmd 0x2b 0f170200000000000000000000000000000000000000000000ffff 673 | | | | | | | + checksum 674 | | | | | | + static 0x0000 675 | | | | +---------------------------------+ Name in ASCII, max. 18 characters 676 | | | + Set name command, 0x0200 677 | | + Length of payload starting w/ next byte incl. checksum 678 | + static start sequence for message, 0x0f 679 | 680 | Notification handle = 0x002e value: 0f 04 02 00 00 03 ff ff 681 | | | | | | + static end sequence of message, 0xffff 682 | | | | | + checksum byte starting with length-byte, ending w/ byte before 683 | | | | + always 0x00 684 | | | + Set name command, 0x0200 685 | | + Length of payload starting w/ next byte incl. checksum 686 | + static start sequence for message, 0x0f 687 | ``` 688 | 689 | ## Get serial 690 | ``` 691 | char-write-cmd 0x2b 0f051100000012ffff 692 | | | | | + checksum 693 | | | | + static 0x0000 694 | | | + Get serial command, 0x1100 695 | | + Length of payload starting w/ next byte incl. checksum 696 | + static start sequence for message, 0x0f 697 | 698 | 699 | Notification handle = 0x002e value: 0f 15 11 00 4d 4c 30 31 44 31 30 30 31 32 30 30 30 30 30 30 700 | Notification handle = 0x002e value: 00 00 64 ff ff 701 | ``` 702 | 703 | The serial number is ASCII coded between bytes 5 and 20. In this example the serial is "ML01D10012000000" 704 | Note that there are two notifications! 705 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Heckie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python3-voltcraft-sem-6000 2 | _"A Python library to manage bluetooth switch, scheduler and smart energy meter Voltcraft SEM 6000 with Linux"_ 3 | 4 | ... based on the API described at https://github.com/Heckie75/voltcraft-sem-6000 5 | 6 | ## Dependencies 7 | 8 | For this library to work bluepy has to be installed. Be sure to run 9 | 10 | ``` 11 | $ pip install -r requirements.txt 12 | ``` 13 | 14 | before using the library. 15 | 16 | 17 | ## Work in progress 18 | 19 | - [x] Authorize with PIN 20 | - [x] Change PIN 21 | - [x] Reset PIN 22 | - [x] Synchronize Date/Time 23 | - [x] Request settings 24 | - [x] Change settings 25 | - [x] Power on/off 26 | - [x] Request timer status 27 | - [x] Set timer 28 | - [x] Stop timer 29 | - [x] Request schedulers 30 | - [x] Set scheduler 31 | - [x] Reset scheduler 32 | - [x] Request random mode status 33 | - [x] Set random mode 34 | - [x] Capture measurement 35 | - [x] Request accumulated measurements for year 36 | - [x] Request accumulated measurements for month 37 | - [x] Request accumulated measurements for day 38 | - [x] Reset accumulated measurement data 39 | - [x] Get device name 40 | - [x] Set device name 41 | - [x] Get device serial 42 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [ ] cleaner API for working with weekdays 2 | - [x] cleaner API for working with timer - rename is_timer_running to is_active 3 | - [x] cleaner API response - replace separate values with isotime or isodatetime 4 | - [x] cleaner API for schedulers - separate fields for isodate and isotime, since date values can be empty 5 | - [x] cleaner API for returning device name - return notification object as response 6 | - [x] introduce set_timer command for explicit target date and time - for restoring settings 7 | - [x] rename led command to nightmode 8 | - [x] cleaner API - use change or set consistently 9 | - [x] move pseudo commands from cli demo to sem6000 module (i.e. reset\_timer, reset\_random_mode) 10 | - [ ] introduce hierarchy in settings response (i.e. reduced-period sub-object) 11 | - [ ] settings backup / restore cli: beautification of JSON 12 | -------------------------------------------------------------------------------- /expected_settings_backup_for_integration_test_hw_v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "device-name": "integrationTest name", 3 | "settings": { 4 | "reduced-period": { 5 | "is-active": true, 6 | "price-in-cent": 23, 7 | "start-isotime": "22:00", 8 | "end-isotime": "06:00" 9 | }, 10 | "normal-price-in-cent": 42, 11 | "is-nightmode-active": false, 12 | "power-limit-in-watt": 2000 13 | }, 14 | "random-mode": { 15 | "is-active": true, 16 | "active-on-weekdays": [ 17 | 0, 18 | 1, 19 | 2, 20 | 3, 21 | 4, 22 | 5, 23 | 6 24 | ], 25 | "start-isotime": "08:00", 26 | "end-isotime": "16:00" 27 | }, 28 | "timer": { 29 | "is-active": true, 30 | "is-action-turn-on": false, 31 | "isodatetime": "2040-01-01T12:34:56" 32 | }, 33 | "scheduler": { 34 | "number-of-schedulers": 4, 35 | "entries": { 36 | "9": { 37 | "is-active": true, 38 | "is-action-turn-on": true, 39 | "repeat-on-weekdays": [ 40 | 0, 41 | 1, 42 | 2, 43 | 3, 44 | 4, 45 | 5, 46 | 6 47 | ], 48 | "isotime": "08:00" 49 | }, 50 | "10": { 51 | "is-active": true, 52 | "is-action-turn-on": false, 53 | "repeat-on-weekdays": [ 54 | 0, 55 | 1, 56 | 2, 57 | 3, 58 | 4, 59 | 5, 60 | 6 61 | ], 62 | "isotime": "10:00" 63 | }, 64 | "11": { 65 | "is-active": true, 66 | "is-action-turn-on": true, 67 | "isodatetime": "2040-01-01T15:00" 68 | }, 69 | "12": { 70 | "is-active": true, 71 | "is-action-turn-on": false, 72 | "isodatetime": "2040-01-01T16:00" 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /expected_settings_backup_for_integration_test_hw_v3.json: -------------------------------------------------------------------------------- 1 | { 2 | "device-name": "integrationTest name", 3 | "settings": { 4 | "reduced-period": { 5 | "is-active": true, 6 | "price-in-cent": 23, 7 | "start-isotime": "22:00", 8 | "end-isotime": "06:00" 9 | }, 10 | "normal-price-in-cent": 42, 11 | "is-nightmode-active": false, 12 | "power-limit-in-watt": 2000 13 | }, 14 | "random-mode": { 15 | "is-active": true, 16 | "active-on-weekdays": [ 17 | 0, 18 | 1, 19 | 2, 20 | 3, 21 | 4, 22 | 5, 23 | 6 24 | ], 25 | "start-isotime": "08:00", 26 | "end-isotime": "16:00" 27 | }, 28 | "timer": { 29 | "is-active": true, 30 | "is-action-turn-on": false, 31 | "isodatetime": "2040-01-01T12:34:56" 32 | }, 33 | "scheduler": { 34 | "number-of-schedulers": 4, 35 | "entries": { 36 | "0": { 37 | "is-active": true, 38 | "is-action-turn-on": false, 39 | "isodatetime": "2040-01-01T16:00" 40 | }, 41 | "1": { 42 | "is-active": true, 43 | "is-action-turn-on": true, 44 | "isodatetime": "2040-01-01T15:00" 45 | }, 46 | "2": { 47 | "is-active": true, 48 | "is-action-turn-on": false, 49 | "repeat-on-weekdays": [ 50 | 0, 51 | 1, 52 | 2, 53 | 3, 54 | 4, 55 | 5, 56 | 6 57 | ], 58 | "isotime": "10:00" 59 | }, 60 | "3": { 61 | "is-active": true, 62 | "is-action-turn-on": true, 63 | "repeat-on-weekdays": [ 64 | 0, 65 | 1, 66 | 2, 67 | 3, 68 | 4, 69 | 5, 70 | 6 71 | ], 72 | "isotime": "08:00" 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /integration-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | usage() { 5 | echo "$0 " 1>&2 6 | } 7 | 8 | main() { 9 | ADDRESS="$1" 10 | PIN="$2" 11 | 12 | SCRIPTDIR="$( dirname "$0" )" 13 | 14 | ORIGINAL_SETTINGS_FILE="original_settings.json" 15 | SETTINGS_FILE_FOR_TEST="settings_for_integration_test.json" 16 | 17 | HARDWARE_VERSION=$( ${SCRIPTDIR}/sem6000-cli-demo.py "$ADDRESS" "$PIN" get_hardware_version | grep -o '[0-9]\+' ) 18 | 19 | EXPECTED_BACKUP_FILE_FOR_TEST="expected_settings_backup_for_integration_test_hw_v2.json" 20 | if [ ${HARDWARE_VERSION} -ge 3 ] 21 | then 22 | EXPECTED_BACKUP_FILE_FOR_TEST="expected_settings_backup_for_integration_test_hw_v3.json" 23 | fi 24 | 25 | TMPFILE="$( mktemp )" 26 | 27 | TEST_FAILED=0 28 | 29 | if [ -f "$SCRIPTDIR/${ORIGINAL_SETTINGS_FILE}" ] 30 | then 31 | echo "$SCRIPTDIR/${ORIGINAL_SETTINGS_FILE} already exists ... aborting" 1>&2 32 | return 1 33 | fi 34 | 35 | echo "Backing up original settings to $SCRIPTDIR/${ORIGINAL_SETTINGS_FILE} ..." 1>&2 36 | if ! "$SCRIPTDIR/sem6000-settings-backup-demo.py" "$ADDRESS" "$PIN" > "$SCRIPTDIR/${ORIGINAL_SETTINGS_FILE}" 37 | then 38 | echo "Failed to backup current settings ... aborting" 1>&2 39 | return 1 40 | fi 41 | echo "" 1>&2 42 | 43 | echo "Running integration test... " 1>&2 44 | "$SCRIPTDIR/sem6000-cli-demo.py" "$ADDRESS" "$PIN" change_date_and_time "2000-01-01T00:00" 45 | "$SCRIPTDIR/sem6000-settings-restore-demo.py" "$ADDRESS" "$PIN" "$SCRIPTDIR/${SETTINGS_FILE_FOR_TEST}" 46 | "$SCRIPTDIR/sem6000-settings-backup-demo.py" "$ADDRESS" "$PIN" > "$TMPFILE" 47 | if ! diff -u "${EXPECTED_BACKUP_FILE_FOR_TEST}" "$TMPFILE" 48 | then 49 | TEST_FAILED=1 50 | echo "FAILED" 1>&2 51 | fi 52 | echo "" 1>&2 53 | 54 | echo "Test reading values that are not covered by backup/restore settings script..." 1>&2 55 | if ! "$SCRIPTDIR/sem6000-read-tests.py" "$ADDRESS" "$PIN" 56 | then 57 | TEST_FAILED=1 58 | echo "FAILED" 1>&2 59 | fi 60 | echo "" 1>&2 61 | 62 | echo "Restoring original settings from $SCRIPTDIR/${ORIGINAL_SETTINGS_FILE} ..." 1>&2 63 | "$SCRIPTDIR/sem6000-settings-restore-demo.py" "$ADDRESS" "$PIN" "$SCRIPTDIR/${ORIGINAL_SETTINGS_FILE}" 64 | "$SCRIPTDIR/sem6000-cli-demo.py" "$ADDRESS" "$PIN" synchronize_date_and_time 65 | echo "" 1>&2 66 | 67 | if [ "${TEST_FAILED}" == "0" ] 68 | then 69 | echo "Test succeeded." 70 | return 0 71 | else 72 | echo "Test failed." 73 | return 1 74 | fi 75 | } 76 | 77 | if [ $# -lt 2 ] 78 | then 79 | usage 80 | exit 1 81 | fi 82 | 83 | main "$@" 84 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bluepy~=1.3 2 | -------------------------------------------------------------------------------- /sem6000-cli-demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from sem6000 import sem6000 4 | from sem6000.message import * 5 | from sem6000 import util 6 | 7 | import datetime 8 | import sys 9 | 10 | 11 | if __name__ == '__main__': 12 | if len(sys.argv) == 2 and sys.argv[1] == 'discover': 13 | devices = sem6000.SEM6000.discover() 14 | for device in devices: 15 | print(device['name'] + '\t' + device['address']) 16 | elif len(sys.argv) < 2: 17 | scriptname = sys.argv[0] 18 | print("Usage:" , file=sys.stderr) 19 | print("\t" + scriptname + " [
] [...]" , file=sys.stderr) 20 | print("\t\taddress:\tAddress of the bluetooth device to connect to, i.e. 00:11:22:33:44:55" , file=sys.stderr) 21 | print("\t\tpin:\t\t4-digit PIN of the device, i.e. 0000" , file=sys.stderr) 22 | print("\t\tcommand:\tOne of the following commands to execute on the device", file=sys.stderr) 23 | print("\t\t\tdiscover", file=sys.stderr) 24 | print("\t\t\t\tScans for devices in range", file=sys.stderr) 25 | print("", file=sys.stderr) 26 | print("\t\t\tget_hardware_version", file=sys.stderr) 27 | print("\t\t\t\tPrints the hardware version of the connected device", file=sys.stderr) 28 | print("\t\t\tchange_pin ", file=sys.stderr) 29 | print("\t\t\t\tChanges the PIN", file=sys.stderr) 30 | print("\t\t\treset_pin", file=sys.stderr) 31 | print("\t\t\t\tResets the PIN to 0000", file=sys.stderr) 32 | print("", file=sys.stderr) 33 | print("\t\t\tpower_on", file=sys.stderr) 34 | print("\t\t\t\tPowers the switch on", file=sys.stderr) 35 | print("\t\t\tpower_off", file=sys.stderr) 36 | print("\t\t\t\tPowers the switch off", file=sys.stderr) 37 | print("", file=sys.stderr) 38 | print("\t\t\tnightmode_on", file=sys.stderr) 39 | print("\t\t\t\tTurn LED always off", file=sys.stderr) 40 | print("\t\t\tnightmode_off", file=sys.stderr) 41 | print("\t\t\t\tTurns the LED on when the switch is turned on", file=sys.stderr) 42 | print("", file=sys.stderr) 43 | print("\t\t\tchange_date_and_time ", file=sys.stderr) 44 | print("\t\t\t\tSets date and time which must be provided in iso format, i.e. 2020-01-01T12:00:00. Setting date and time also starts collection of consumption data", file=sys.stderr) 45 | print("\t\t\tsynchronize_date_and_time", file=sys.stderr) 46 | print("\t\t\t\tSets date and time of the device to the current system time", file=sys.stderr) 47 | print("", file=sys.stderr) 48 | print("\t\t\trequest_settings", file=sys.stderr) 49 | print("\t\t\t\tRequest current settings for power limit, prices and reduced period times", file=sys.stderr) 50 | print("\t\t\tchange_power_limit ", file=sys.stderr) 51 | print("\t\t\t\tSets the power limit in watt when the switch should be automatically turned off", file=sys.stderr) 52 | print("\t\t\tchange_prices ", file=sys.stderr) 53 | print("\t\t\t\tSet prices for normal and reduced period", file=sys.stderr) 54 | print("\t\t\tchange_reduced_period ", file=sys.stderr) 55 | print("\t\t\t\tSet begin and end time of the reduced period", file=sys.stderr) 56 | print("", file=sys.stderr) 57 | print("\t\t\trequest_timer_status", file=sys.stderr) 58 | print("\t\t\t\tRequest the current status of the timer", file=sys.stderr) 59 | print("\t\t\tactivate_timer ", file=sys.stderr) 60 | print("\t\t\t\tActivates the timer to execute the switch action after the specified delay", file=sys.stderr) 61 | print("\t\t\tactivate_timer_at ", file=sys.stderr) 62 | print("\t\t\t\tActivates the timer to execute the switch action at the specified date and time", file=sys.stderr) 63 | print("\t\t\treset_timer", file=sys.stderr) 64 | print("\t\t\t\tResets/stops the timer", file=sys.stderr) 65 | print("", file=sys.stderr) 66 | print("\t\t\trequest_scheduler", file=sys.stderr) 67 | print("\t\t\t\tRequest all scheduler entries", file=sys.stderr) 68 | print("\t\t\tadd_onetime_scheduler ", file=sys.stderr) 69 | print("\t\t\t\tAdd a scheduler entry occuring at a specific date and time, i.e. True True 2020-01-01T12:00", file=sys.stderr) 70 | print("\t\t\tedit_onetime_scheduler ", file=sys.stderr) 71 | print("\t\t\t\tEdit an existing scheduler entry occuring at a specific date and time, i.e. 12 True True 2020-01-01T12:00", file=sys.stderr) 72 | print("\t\t\tadd_repeated_scheduler ", file=sys.stderr) 73 | print("\t\t\t\tAdd a scheduler entry that will be repeated regulary, i.e. True True Mon,Wed,Sun 12:00", file=sys.stderr) 74 | print("\t\t\tedit_repeated_scheduler ", file=sys.stderr) 75 | print("\t\t\t\tEdit an existing scheduler entry that will be repeated regulary, i.e. 12 True True Mon,Wed,Fri 18:00", file=sys.stderr) 76 | print("\t\t\tremove_scheduler ", file=sys.stderr) 77 | print("\t\t\t\tRemoves a scheduler entry", file=sys.stderr) 78 | print("", file=sys.stderr) 79 | print("\t\t\trequest_random_mode_status", file=sys.stderr) 80 | print("\t\t\t\tRequests current status of the random mode", file=sys.stderr) 81 | print("\t\t\tchange_random_mode ", file=sys.stderr) 82 | print("\t\t\t\tSets random mode, i.e. True Mon,Wed,Sun 22:00 04:00", file=sys.stderr) 83 | print("\t\t\treset_random_mode", file=sys.stderr) 84 | print("\t\t\t\tResets random mode", file=sys.stderr) 85 | print("", file=sys.stderr) 86 | print("\t\t\trequest_measurement", file=sys.stderr) 87 | print("\t\t\t\tRequest current measurements", file=sys.stderr) 88 | print("", file=sys.stderr) 89 | print("\t\t\trequest_consumptions_of_last_12_months", file=sys.stderr) 90 | print("\t\t\t\tRequest accumulated measurements of last 12 months", file=sys.stderr) 91 | print("\t\t\trequest_consumptions_of_last_30_days", file=sys.stderr) 92 | print("\t\t\t\tRequest accumulated measurements of last 30 days", file=sys.stderr) 93 | print("\t\t\trequest_consumptions_of_last_23_hours", file=sys.stderr) 94 | print("\t\t\t\tRequest accumulated measurements of last 23 hours", file=sys.stderr) 95 | print("\t\t\treset_consumption", file=sys.stderr) 96 | print("\t\t\t\tResets collected consumption data", file=sys.stderr) 97 | print("", file=sys.stderr) 98 | print("\t\t\trequest_device_name", file=sys.stderr) 99 | print("\t\t\t\tRequests the device name", file=sys.stderr) 100 | print("\t\t\tchange_device_name ", file=sys.stderr) 101 | print("\t\t\t\tChanges the device name", file=sys.stderr) 102 | print("\t\t\tfactory_reset", file=sys.stderr) 103 | print("\t\t\t\tResets the device back to factory defaults", file=sys.stderr) 104 | print("\t\t\trequest_device_serial", file=sys.stderr) 105 | print("\t\t\t\tRequest the serial number of the device", file=sys.stderr) 106 | else: 107 | deviceAddr = sys.argv[1] 108 | pin = sys.argv[2] 109 | cmd = sys.argv[3] 110 | 111 | sem6000 = sem6000.SEM6000(deviceAddr, debug=True) 112 | 113 | if cmd != 'reset_pin' and cmd != 'get_device_name' and cmd != 'get_hardware_version': 114 | sem6000.authorize(pin) 115 | 116 | if cmd == 'get_hardware_version': 117 | print("Hardware version: " + str(sem6000.hardware_version)) 118 | elif cmd == 'change_pin': 119 | sem6000.change_pin(sys.argv[4]) 120 | elif cmd == 'reset_pin': 121 | sem6000.reset_pin() 122 | elif cmd == 'power_on': 123 | sem6000.power_on() 124 | elif cmd == 'power_off': 125 | sem6000.power_off() 126 | elif cmd == 'nightmode_on': 127 | sem6000.nightmode_on() 128 | elif cmd == 'nightmode_off': 129 | sem6000.nightmode_off() 130 | elif cmd == 'change_date_and_time': 131 | sem6000.change_date_and_time(sys.argv[4]) 132 | elif cmd == 'synchronize_date_and_time': 133 | sem6000.change_date_and_time(datetime.datetime.now().isoformat()) 134 | elif cmd == 'request_settings': 135 | response = sem6000.request_settings() 136 | assert isinstance(response, SettingsRequestedNotification) 137 | 138 | print("Settings:") 139 | if response.is_reduced_period: 140 | print("\tReduced mode:\t\t\tOn") 141 | else: 142 | print("\tReduced mode:\t\t\tOff") 143 | 144 | print("\tNormal price:\t\t\t{:.2f} EUR".format(response.normal_price_in_cent/100)) 145 | print("\tReduced period price:\t\t{:.2f} EUR".format(response.reduced_period_price_in_cent/100)) 146 | 147 | print("\tRecuced period start:\t\t{}".format(response.reduced_period_start_isotime)) 148 | print("\tRecuced period end:\t\t{}".format(response.reduced_period_end_isotime)) 149 | 150 | if response.is_nightmode_active: 151 | print("\tNightmode state:\t\tOn") 152 | else: 153 | print("\tNightmode state:\t\tOff") 154 | 155 | print("\tPower limit:\t\t\t{} W".format(response.power_limit_in_watt)) 156 | elif cmd == 'change_power_limit': 157 | sem6000.change_power_limit(power_limit_in_watt=sys.argv[4]) 158 | elif cmd == 'change_prices': 159 | sem6000.change_prices(normal_price_in_cent=sys.argv[4], reduced_period_price_in_cent=sys.argv[5]) 160 | elif cmd == 'change_reduced_period': 161 | sem6000.change_reduced_period(is_active=sys.argv[4], start_isotime=sys.argv[5], end_isotime=sys.argv[6]) 162 | elif cmd == 'request_timer_status': 163 | response = sem6000.request_timer_status() 164 | assert isinstance(response, TimerStatusRequestedNotification) 165 | 166 | original_timer_length = datetime.timedelta(seconds=response.original_timer_length_in_seconds) 167 | 168 | print("Timer Status:") 169 | if response.is_active: 170 | now = datetime.datetime.now() 171 | now = datetime.datetime(now.year, now.month, now.day, now.hour, now.minute, now.second) 172 | 173 | dt = datetime.datetime.fromisoformat(response.target_isodatetime) 174 | time_left = (dt - now) 175 | 176 | print("\tTimer state:\t\tOn") 177 | print("\tTime left:\t\t" + str(time_left)) 178 | if response.is_action_turn_on: 179 | print("\tAction:\t\t\tTurn On") 180 | else: 181 | print("\tAction:\t\t\tTurn Off") 182 | else: 183 | print("\tTimer state:\t\tOff") 184 | 185 | print("\tOriginal timer length:\t" + str(original_timer_length)) 186 | elif cmd == 'activate_timer': 187 | is_action_turn_on = sys.argv[4] 188 | delay_isotime = sys.argv[5] 189 | 190 | sem6000.activate_timer(is_action_turn_on=is_action_turn_on, delay_isotime=delay_isotime) 191 | elif cmd == 'activate_timer_at': 192 | is_action_turn_on = sys.argv[4] 193 | target_isodatetime = sys.argv[5] 194 | 195 | sem6000.activate_timer_at(is_action_turn_on=is_action_turn_on, target_isodatetime=target_isodatetime) 196 | elif cmd == 'reset_timer': 197 | sem6000.reset_timer() 198 | elif cmd == 'request_scheduler': 199 | response = sem6000.request_scheduler() 200 | 201 | print("Schedulers:") 202 | for i in range(len(response.scheduler_entries)): 203 | scheduler_entry = response.scheduler_entries[i] 204 | scheduler = scheduler_entry.scheduler 205 | 206 | print("\t#" + str(scheduler_entry.slot_id)) 207 | 208 | if scheduler.is_active: 209 | print("\tActive:\t\t\tOn") 210 | else: 211 | print("\tActive:\t\t\tOff") 212 | 213 | if scheduler.is_action_turn_on: 214 | print("\tAction:\t\t\tTurn On") 215 | else: 216 | print("\tAction:\t\t\tTurn Off") 217 | 218 | dt = datetime.datetime.fromisoformat(scheduler.isodatetime) 219 | if scheduler.repeat_on_weekdays: 220 | weekday_formatter = lambda w: w.name 221 | repeat_on_weekdays = util._format_list_of_objects(weekday_formatter, scheduler.repeat_on_weekdays) 222 | print("\tRepeat on weekdays:\t" + repeat_on_weekdays) 223 | else: 224 | date = dt.date() 225 | print("\tDate:\t\t\t" + str(date)) 226 | 227 | print("\tTime:\t\t\t" + dt.time().isoformat(timespec='minutes')) 228 | print("") 229 | elif cmd == 'add_onetime_scheduler': 230 | is_active = sys.argv[4] 231 | is_action_turn_on = sys.argv[5] 232 | isodatetime = sys.argv[6] 233 | 234 | response = sem6000.add_onetime_scheduler(is_active=is_active, is_action_turn_on=is_action_turn_on, isodatetime=isodatetime) 235 | elif cmd == 'edit_onetime_scheduler': 236 | slot_id = sys.argv[4] 237 | is_active = sys.argv[5] 238 | is_action_turn_on = sys.argv[6] 239 | isodatetime = sys.argv[7] 240 | 241 | response = sem6000.edit_onetime_scheduler(slot_id=slot_id, is_active=is_active, is_action_turn_on=is_action_turn_on, isodatetime=isodatetime) 242 | elif cmd == 'add_repeated_scheduler': 243 | is_active = sys.argv[4] 244 | is_action_turn_on = sys.argv[5] 245 | repeat_on_weekdays=sys.argv[6] 246 | isotime = sys.argv[7] 247 | 248 | response = sem6000.add_repeated_scheduler(is_active=is_active, is_action_turn_on=is_action_turn_on, repeat_on_weekdays=repeat_on_weekdays, isotime=isotime) 249 | elif cmd == 'edit_repeated_scheduler': 250 | slot_id = sys.argv[4] 251 | is_active = sys.argv[5] 252 | is_action_turn_on = sys.argv[6] 253 | repeat_on_weekdays=sys.argv[7] 254 | isotime = sys.argv[8] 255 | 256 | response = sem6000.edit_repeated_scheduler(slot_id=slot_id, is_active=is_active, is_action_turn_on=is_action_turn_on, repeat_on_weekdays=repeat_on_weekdays, isotime=isotime) 257 | elif cmd == 'remove_scheduler': 258 | slot_id = sys.argv[4] 259 | 260 | sem6000.remove_scheduler(slot_id=slot_id) 261 | elif cmd == 'request_random_mode_status': 262 | response = sem6000.request_random_mode_status() 263 | 264 | print("Random mode status:") 265 | if response.is_active: 266 | print("\tActive:\t\t\tOn") 267 | else: 268 | print("\tActive:\t\t\tOff") 269 | 270 | weekday_formatter = lambda w: w.name 271 | active_on_weekdays = util._format_list_of_objects(weekday_formatter, response.active_on_weekdays) 272 | 273 | start_time = response.start_isotime 274 | end_time = response.end_isotime 275 | 276 | print("\tActive on weekdays:\t" + active_on_weekdays) 277 | print("\tStart time:\t\t" + str(start_time)) 278 | print("\tEnd time:\t\t" + str(end_time)) 279 | print("") 280 | elif cmd == 'change_random_mode': 281 | active_on_weekdays = sys.argv[4] 282 | start_isotime = sys.argv[5] 283 | end_isotime = sys.argv[6] 284 | 285 | sem6000.change_random_mode(active_on_weekdays=active_on_weekdays, start_isotime=start_isotime, end_isotime=end_isotime) 286 | elif cmd == 'reset_random_mode': 287 | sem6000.reset_random_mode() 288 | elif cmd == 'request_measurement': 289 | response = sem6000.request_measurement() 290 | 291 | print("Current measurement:") 292 | if response.is_power_active: 293 | print("\tPower:\t\t\tOn") 294 | else: 295 | print("\tPower:\t\t\tOff") 296 | 297 | power_in_milliwatt = response.power_in_milliwatt 298 | voltage_in_volt = response.voltage_in_volt 299 | current_in_milliampere = response.current_in_milliampere 300 | frequency_in_hertz = response.frequency_in_hertz 301 | total_consumption_in_kilowatt_hour = response.total_consumption_in_kilowatt_hour 302 | 303 | print("\tPower:\t\t\t" + str(power_in_milliwatt) + " mW") 304 | print("\tVoltage:\t\t" + str(voltage_in_volt) + " V") 305 | print("\tCurrent:\t\t" + str(current_in_milliampere) + " mA") 306 | print("\tFrequency:\t\t" + str(frequency_in_hertz) + " Hz") 307 | print("\tTotal consumption:\t" + str(total_consumption_in_kilowatt_hour) + " kWh") 308 | elif cmd == 'request_consumptions_of_last_12_months': 309 | response = sem6000.request_consumption_of_last_12_months() 310 | now = datetime.datetime.now() 311 | 312 | print("Consumptions of last 12 months") 313 | for i in range(len(response.consumption_n_months_ago_in_watt_hour)): 314 | if response.consumption_n_months_ago_in_watt_hour[i] is None: 315 | continue 316 | 317 | year = now.year 318 | month = now.month - i 319 | if month < 1: 320 | month += 12 321 | year -= 1 322 | 323 | print("\t" + util._format_year_and_month(year, month) + ":\t" + str(response.consumption_n_months_ago_in_watt_hour[i]) + " Wh") 324 | elif cmd == 'request_consumptions_of_last_30_days': 325 | response = sem6000.request_consumption_of_last_30_days() 326 | now = datetime.datetime.now().date() 327 | 328 | print("Consumptions of last 12 months") 329 | for i in range(len(response.consumption_n_days_ago_in_watt_hour)): 330 | if response.consumption_n_days_ago_in_watt_hour[i] is None: 331 | continue 332 | 333 | d = now - datetime.timedelta(i) 334 | print("\t" + d.isoformat() + ":\t" + str(response.consumption_n_days_ago_in_watt_hour[i]) + " Wh") 335 | elif cmd == 'request_consumptions_of_last_23_hours': 336 | response = sem6000.request_consumption_of_last_23_hours() 337 | now = datetime.datetime.now().time() 338 | 339 | print("Consumptions of last 23 hours") 340 | for i in range(len(response.consumption_n_hours_ago_in_watt_hour)): 341 | if response.consumption_n_hours_ago_in_watt_hour[i] is None: 342 | continue 343 | 344 | hour = now.hour - i 345 | if hour < 0: 346 | hour += 24 347 | 348 | isotime = datetime.time(hour, 0).isoformat(timespec='minutes') 349 | 350 | print("\t" + isotime + ":\t" + str(response.consumption_n_hours_ago_in_watt_hour[i]) + " Wh") 351 | elif cmd == 'reset_consumption': 352 | sem6000.reset_consumption() 353 | elif cmd == 'request_device_name': 354 | response = sem6000.request_device_name() 355 | 356 | print("Device-Name:\t" + response.device_name) 357 | elif cmd == 'change_device_name': 358 | new_name = sys.argv[4] 359 | 360 | sem6000.change_device_name(new_name=new_name) 361 | elif cmd == 'factory_reset': 362 | sem6000.factory_reset() 363 | elif cmd == 'request_device_serial': 364 | response = sem6000.request_device_serial() 365 | 366 | print("Device-Serial:\t" + str(response.serial)) 367 | else: 368 | print("Invalid/unknown command: " + cmd, file=sys.stderr) 369 | -------------------------------------------------------------------------------- /sem6000-read-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | 5 | from sem6000 import sem6000 6 | from sem6000.message import * 7 | 8 | if len(sys.argv) <= 1: 9 | print("Usage: " + sys.argv[0] + " ", file=sys.stderr) 10 | else: 11 | address = sys.argv[1] 12 | pin = sys.argv[2] 13 | 14 | device = sem6000.SEM6000(address, pin, debug=True) 15 | 16 | consumption_of_last_12_months_response = device.request_consumption_of_last_12_months() 17 | assert len(consumption_of_last_12_months_response.consumption_n_months_ago_in_watt_hour) == 1+12 18 | 19 | consumption_of_last_30_days_response = device.request_consumption_of_last_30_days() 20 | assert len(consumption_of_last_30_days_response.consumption_n_days_ago_in_watt_hour) == 1+30 21 | 22 | consumption_of_last_23_days_response = device.request_consumption_of_last_23_hours() 23 | assert len(consumption_of_last_23_days_response.consumption_n_hours_ago_in_watt_hour) == 1+23 24 | 25 | measurement_response = device.request_measurement() 26 | assert measurement_response.power_in_milliwatt >= 0 27 | assert measurement_response.current_in_milliampere >= 0 28 | assert measurement_response.voltage_in_volt > 0 29 | assert measurement_response.frequency_in_hertz > 0 30 | assert measurement_response.is_power_active 31 | 32 | timer_status_response = device.request_timer_status() 33 | assert not timer_status_response.is_active is None 34 | 35 | if device.hardware_version <= 2: 36 | device_serial_response = device.request_device_serial() 37 | assert len(device_serial_response.serial) > 0 38 | 39 | 40 | -------------------------------------------------------------------------------- /sem6000-settings-backup-demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import json 4 | import sys 5 | 6 | from sem6000 import sem6000 7 | from sem6000.message import * 8 | 9 | if len(sys.argv) <= 1: 10 | print("Usage: " + sys.argv[0] + " ", file=sys.stderr) 11 | else: 12 | address = sys.argv[1] 13 | pin = sys.argv[2] 14 | 15 | device = sem6000.SEM6000(address, pin, debug=True) 16 | 17 | device_name_response = device.request_device_name() 18 | 19 | settings_response = device.request_settings() 20 | timer_response = device.request_timer_status() 21 | random_mode_response = device.request_random_mode_status() 22 | scheduler_response = device.request_scheduler() 23 | 24 | data = {} 25 | data["device-name"] = device_name_response.device_name 26 | 27 | data["settings"] = { 28 | "reduced-period": { 29 | "is-active": settings_response.is_reduced_period, 30 | "price-in-cent": settings_response.reduced_period_price_in_cent, 31 | "start-isotime": settings_response.reduced_period_start_isotime, 32 | "end-isotime": settings_response.reduced_period_end_isotime 33 | }, 34 | "normal-price-in-cent": settings_response.normal_price_in_cent, 35 | "is-nightmode-active": settings_response.is_nightmode_active, 36 | "power-limit-in-watt": settings_response.power_limit_in_watt 37 | } 38 | 39 | weekdays = [] 40 | for w in random_mode_response.active_on_weekdays: 41 | weekdays.append(w.value) 42 | 43 | data["random-mode"] = { 44 | "is-active": random_mode_response.is_active, 45 | "active-on-weekdays": weekdays, 46 | "start-isotime": random_mode_response.start_isotime, 47 | "end-isotime": random_mode_response.end_isotime 48 | } 49 | 50 | data["timer"] = { 51 | "is-active": timer_response.is_active, 52 | "is-action-turn-on": timer_response.is_action_turn_on, 53 | "isodatetime": timer_response.target_isodatetime 54 | } 55 | 56 | data["scheduler"] = { 57 | "number-of-schedulers": scheduler_response.number_of_schedulers, 58 | "entries": { 59 | } 60 | } 61 | 62 | d = data["scheduler"]["entries"] 63 | for entry in scheduler_response.scheduler_entries: 64 | scheduler = entry.scheduler 65 | weekdays = [] 66 | for w in scheduler.repeat_on_weekdays: 67 | weekdays.append(w.value) 68 | 69 | dt = datetime.datetime.fromisoformat(scheduler.isodatetime) 70 | 71 | if not len(weekdays): 72 | d[entry.slot_id] = { 73 | "is-active": scheduler.is_active, 74 | "is-action-turn-on": scheduler.is_action_turn_on, 75 | "isodatetime": scheduler.isodatetime 76 | } 77 | else: 78 | d[entry.slot_id] = { 79 | "is-active": scheduler.is_active, 80 | "is-action-turn-on": scheduler.is_action_turn_on, 81 | "repeat-on-weekdays": weekdays, 82 | "isotime": dt.time().isoformat(timespec='minutes') 83 | } 84 | 85 | json.dump(data, sys.stdout, indent=True) 86 | print("") 87 | 88 | -------------------------------------------------------------------------------- /sem6000-settings-restore-demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import datetime 4 | import json 5 | import time 6 | import sys 7 | 8 | from sem6000 import sem6000 9 | from sem6000.message import * 10 | 11 | 12 | if len(sys.argv) <= 1: 13 | print("Usage: " + sys.argv[0] + " ", file=sys.stderr) 14 | else: 15 | address = sys.argv[1] 16 | pin = sys.argv[2] 17 | json_file = sys.argv[3] 18 | 19 | f = open(json_file) 20 | data = json.load(f) 21 | f.close() 22 | 23 | device = sem6000.SEM6000(address, pin, debug=True) 24 | device.factory_reset() 25 | device.disconnect() 26 | 27 | time.sleep(3) 28 | 29 | device = sem6000.SEM6000(address, "0000", debug=True) 30 | device.change_pin(pin) 31 | 32 | device.change_device_name(data["device-name"]) 33 | 34 | device.change_prices(data["settings"]["normal-price-in-cent"], data["settings"]["reduced-period"]["price-in-cent"]) 35 | device.change_reduced_period(data["settings"]["reduced-period"]["is-active"], data["settings"]["reduced-period"]["start-isotime"], data["settings"]["reduced-period"]["end-isotime"]) 36 | 37 | if data["settings"]["is-nightmode-active"]: 38 | device.nightmode_on() 39 | else: 40 | device.nightmode_off() 41 | 42 | device.change_power_limit(data["settings"]["power-limit-in-watt"]) 43 | 44 | if data["random-mode"]["is-active"]: 45 | device.change_random_mode(data["random-mode"]["active-on-weekdays"], data["random-mode"]["start-isotime"], data["random-mode"]["end-isotime"]) 46 | else: 47 | device.reset_random_mode() 48 | 49 | if data["timer"]["is-active"]: 50 | device.activate_timer_at(data["timer"]["is-action-turn-on"], data["timer"]["isodatetime"]) 51 | 52 | slot_ids = list(data["scheduler"]["entries"].keys()) 53 | slot_ids.reverse() 54 | for slot_id in slot_ids: 55 | scheduler = data["scheduler"]["entries"][slot_id] 56 | 57 | if not "repeat-on-weekdays" in scheduler: 58 | device.add_onetime_scheduler(scheduler["is-active"], scheduler["is-action-turn-on"], scheduler["isodatetime"]) 59 | else: 60 | device.add_repeated_scheduler(scheduler["is-active"], scheduler["is-action-turn-on"], scheduler["repeat-on-weekdays"], scheduler["isotime"]) 61 | -------------------------------------------------------------------------------- /sem6000/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moormaster/python3-voltcraft-sem6000/aa6835cf44ddc73badf6c4ec7ba121c891537e7a/sem6000/__init__.py -------------------------------------------------------------------------------- /sem6000/bluetooth_lowenergy_interface/abstract_interface.py: -------------------------------------------------------------------------------- 1 | from abc import * 2 | 3 | class AbstractBluetoothInterface(ABC): 4 | def __init__(self, mac_address=None, bluetooth_device='hci0'): 5 | self.mac_addess = mac_address 6 | self.bluetooth_device = bluetooth_device 7 | 8 | self._is_notifications_enabled = False 9 | self._notification_handler = [] 10 | 11 | def enable_notifications(self): 12 | '''Enables reception of notifications from the device''' 13 | 14 | self._is_notifications_enabled = True 15 | 16 | def disable_notifications(self): 17 | '''Disables reception of notifications from the device''' 18 | 19 | self._is_notifications_enabled = False 20 | 21 | @abstractmethod 22 | def discover(self, timeout, service_uuids=[]): 23 | ''' 24 | Returns a list of discovered devices. 25 | 26 | Parameters: 27 | timeout (int): Maximum amount of seconds to wait for device advertisements 28 | service_uuds (list of str): When given only devices advertising one of these services are returned 29 | 30 | Returns: 31 | A list of dictionaries having keys 'address' and 'name' 32 | ''' 33 | 34 | pass 35 | 36 | @abstractmethod 37 | def connect(self, mac_address): 38 | '''Connects to the given device''' 39 | 40 | pass 41 | 42 | @abstractmethod 43 | def disconnect(self): 44 | '''Disconnects from the currently connected device''' 45 | 46 | pass 47 | 48 | @abstractmethod 49 | def set_mtu(self, mtu): 50 | '''Sets the desired MTU size of packages transmitted/received''' 51 | 52 | pass 53 | 54 | @abstractmethod 55 | def is_connected(self): 56 | '''Returns True if connected to a device''' 57 | 58 | pass 59 | 60 | @abstractmethod 61 | def write_to_characteristic(self, uuid, data): 62 | ''' 63 | Send data to the characteristics identified by uuid of the currently connected device 64 | 65 | Parameters: 66 | uuid (str): UUID of the form 00000000-0000-0000-0000-000000000000 67 | data (bytes): data to send 68 | ''' 69 | 70 | pass 71 | 72 | @abstractmethod 73 | def read_from_characteristic(self, uuid): 74 | ''' 75 | Read data from the characteristics identified by uuid 76 | 77 | Parameters: 78 | uuid (str): UUID of the form 00000000-0000-0000-0000-000000000000 79 | ''' 80 | 81 | pass 82 | 83 | @abstractmethod 84 | def wait_for_notifications(self, timeout): 85 | ''' 86 | Waits for notifications 87 | 88 | Parameters: 89 | timeout (int): Maximum amount of seconds to wait for incoming notifications 90 | 91 | Returns: 92 | True if a notification was received 93 | False if no notification was received 94 | ''' 95 | 96 | pass 97 | 98 | def add_notification_handler(self, notification_handler): 99 | ''' 100 | Registers a callable object to handle incoming notifications 101 | 102 | Parameters: 103 | notification_handler (callable): Callable object which is being called with (characteristic_uuid, data) when a notification was received 104 | ''' 105 | 106 | self._notification_handler.append(notification_handler) 107 | 108 | def _send_notification_to_handlers(self, characteristic_uuid, data): 109 | for handler in self._notification_handler: 110 | handler(characteristic_uuid, data) 111 | 112 | -------------------------------------------------------------------------------- /sem6000/bluetooth_lowenergy_interface/bluepy_interface.py: -------------------------------------------------------------------------------- 1 | from . import abstract_interface 2 | from .timeout_decorator import * 3 | 4 | from bluepy import btle 5 | 6 | import sys 7 | 8 | class BluePyBtLeDelegate(btle.DefaultDelegate): 9 | def __init__(self, bluepy_bluetooth_interface): 10 | btle.DefaultDelegate.__init__(self) 11 | 12 | self._bluepy_bluetooth_interface = bluepy_bluetooth_interface 13 | 14 | def handleNotification(self, cHandle, data): 15 | self._bluepy_bluetooth_interface._handle_notification(cHandle, data) 16 | 17 | 18 | class BluePyBtLeInterface(abstract_interface.AbstractBluetoothInterface): 19 | def __init__(self, mac_address=None, bluetooth_device='hci0'): 20 | abstract_interface.AbstractBluetoothInterface.__init__(self, mac_address, bluetooth_device) 21 | 22 | self._peripheral = None 23 | self._delegate = BluePyBtLeDelegate(self) 24 | 25 | self._characteristic_by_uuid = {} 26 | self._characteristic_by_bluepy_handle = {} 27 | 28 | @DisconnectAfterTimeout(300) 29 | def _get_characteristic(self, uuid): 30 | if uuid in self._characteristic_by_uuid: 31 | characteristic = self._characteristic_by_uuid[uuid] 32 | else: 33 | characteristic = self._peripheral.getCharacteristics(uuid=uuid)[0] 34 | 35 | self._characteristic_by_uuid[str(characteristic.uuid)] = characteristic 36 | self._characteristic_by_bluepy_handle[characteristic.valHandle] = characteristic 37 | 38 | return characteristic 39 | 40 | @DisconnectAfterTimeout(300) 41 | def _get_characteristic_by_bluepy_handle(self, bluepy_handle): 42 | if bluepy_handle in self._characteristic_by_bluepy_handle: 43 | characteristic = self._characteristic_by_bluepy_handle[bluepy_handle] 44 | else: 45 | characteristics = self._peripheral.getCharacteristics() 46 | 47 | characteristic = None 48 | for c in characteristics: 49 | if c.valHandle == bluepy_handle: 50 | characteristic = c 51 | break 52 | 53 | self._characteristic_by_uuid[str(characteristic.uuid)] = characteristic 54 | self._characteristic_by_bluepy_handle[characteristic.valHandle] = characteristic 55 | 56 | return characteristic 57 | 58 | def _handle_notification(self, characteristic_bluepy_handle, data): 59 | characteristic = self._get_characteristic_by_bluepy_handle(characteristic_bluepy_handle) 60 | uuid = str(characteristic.uuid) 61 | 62 | self._send_notification_to_handlers(uuid, data) 63 | 64 | def discover(self, timeout, service_uuids=[]): 65 | result = [] 66 | 67 | scanner = btle.Scanner() 68 | scanner_results = scanner.scan(timeout) 69 | 70 | for device in scanner_results: 71 | address = device.addr 72 | 73 | scanned_incomplete_16b_service_uuids = device.getValueText(btle.ScanEntry.INCOMPLETE_16B_SERVICES) 74 | scanned_complete_16b_service_uuids = device.getValueText(btle.ScanEntry.COMPLETE_16B_SERVICES) 75 | 76 | scanned_incomplete_32b_service_uuids = device.getValueText(btle.ScanEntry.INCOMPLETE_32B_SERVICES) 77 | scanned_complete_32b_service_uuids = device.getValueText(btle.ScanEntry.COMPLETE_32B_SERVICES) 78 | 79 | scanned_incomplete_128b_service_uuids = device.getValueText(btle.ScanEntry.INCOMPLETE_128B_SERVICES) 80 | scanned_complete_128b_service_uuids = device.getValueText(btle.ScanEntry.COMPLETE_128B_SERVICES) 81 | 82 | complete_local_name = device.getValueText(btle.ScanEntry.COMPLETE_LOCAL_NAME) 83 | 84 | if len(service_uuids) > 0: 85 | is_services_matching = False 86 | for uuid in service_uuids: 87 | if not scanned_incomplete_16b_service_uuids is None and uuid in scanned_incomplete_16b_service_uuids: 88 | is_services_matching = True 89 | if not scanned_complete_16b_service_uuids is None and uuid in scanned_complete_16b_service_uuids: 90 | is_services_matching = True 91 | 92 | if not scanned_incomplete_32b_service_uuids is None and uuid in scanned_incomplete_32b_service_uuids: 93 | is_services_matching = True 94 | if not scanned_complete_32b_service_uuids is None and uuid in scanned_complete_32b_service_uuids: 95 | is_services_matching = True 96 | 97 | if not scanned_incomplete_128b_service_uuids is None and uuid in scanned_incomplete_128b_service_uuids: 98 | is_services_matching = True 99 | if not scanned_complete_128b_service_uuids is None and uuid in scanned_complete_128b_service_uuids: 100 | is_services_matching = True 101 | 102 | if not is_services_matching: 103 | continue 104 | 105 | result.append({'address': address, 'name': complete_local_name}) 106 | 107 | return result 108 | 109 | def connect(self, mac_address): 110 | self._peripheral = btle.Peripheral().withDelegate(self._delegate) 111 | try: 112 | iface = int(self.bluetooth_device.replace("hci", "")) 113 | self._peripheral.connect(mac_address, btle.ADDR_TYPE_PUBLIC, iface) 114 | except btle.BTLEException as e: 115 | self._peripheral = None 116 | raise e 117 | 118 | def disconnect(self): 119 | self._characteristic_by_uuid.clear() 120 | self._characteristic_by_bluepy_handle.clear() 121 | 122 | if self.is_connected(): 123 | self._peripheral.disconnect() 124 | 125 | def is_connected(self): 126 | if self._peripheral is None: 127 | return False 128 | 129 | try: 130 | if self._peripheral.getState() != "conn": 131 | return False 132 | except btle.BTLEInternalError as e: 133 | return False 134 | 135 | return True 136 | 137 | @DisconnectAfterTimeout(300) 138 | def set_mtu(self, mtu): 139 | if self._peripheral is None: 140 | return False 141 | 142 | return self._peripheral.setMTU(mtu) 143 | 144 | @DisconnectAfterTimeout(300) 145 | def write_to_characteristic(self, uuid, data): 146 | characteristic = self._get_characteristic(uuid) 147 | 148 | return characteristic.write(data, self._is_notifications_enabled) 149 | 150 | @DisconnectAfterTimeout(300) 151 | def read_from_characteristic(self, uuid): 152 | characteristic = self._get_characteristic(uuid) 153 | 154 | return characteristic.read() 155 | 156 | @DisconnectAfterTimeout(300) 157 | def wait_for_notifications(self, timeout=None): 158 | if not timeout is None: 159 | return self._peripheral.waitForNotifications(timeout) 160 | else: 161 | return self._peripheral.waitForNotifications() 162 | 163 | -------------------------------------------------------------------------------- /sem6000/bluetooth_lowenergy_interface/timeout_decorator.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | def DisconnectAfterTimeout(timeout): 4 | def Decorator(function): 5 | def decorated_function(*s, **d): 6 | def disconnect(): 7 | disconnectable = s[0] 8 | disconnectable.disconnect() 9 | 10 | timer = threading.Timer(timeout, disconnect) 11 | timer.start() 12 | 13 | return_value = None 14 | try: 15 | return_value = function(*s, **d) 16 | finally: 17 | timer.cancel() 18 | 19 | return return_value 20 | 21 | return decorated_function 22 | 23 | return Decorator 24 | 25 | -------------------------------------------------------------------------------- /sem6000/encoder.py: -------------------------------------------------------------------------------- 1 | from .message import * 2 | 3 | import datetime 4 | 5 | class MessageEncoder(): 6 | def _encode_message(self, payload, suffix=b'\xff\xff'): 7 | message = b'\x0f' 8 | 9 | message += (len(payload)+1).to_bytes(1, 'big') 10 | message += payload 11 | 12 | message += ((1+sum(payload)) & 0xff).to_bytes(1, 'big') 13 | message += suffix 14 | 15 | return message 16 | 17 | def _encode_pin(self, pin): 18 | pin_bytes = b'' 19 | for i in pin: 20 | pin_bytes += int(i).to_bytes(1, 'big') 21 | 22 | return pin_bytes 23 | 24 | def _encode_scheduler(self, scheduler): 25 | is_active = b'\x00' 26 | if scheduler.is_active: 27 | is_active = b'\x01' 28 | 29 | is_action_turn_on = b'\x00' 30 | if scheduler.is_action_turn_on: 31 | is_action_turn_on = b'\x01' 32 | 33 | repeat_on_weekdays = 0 34 | for weekday in scheduler.repeat_on_weekdays: 35 | repeat_on_weekdays += 2**weekday.value 36 | repeat_on_weekdays = repeat_on_weekdays.to_bytes(1, 'big') 37 | 38 | d = datetime.datetime.fromisoformat(scheduler.isodatetime) 39 | 40 | year = (d.year % 100).to_bytes(1, 'big') 41 | month = d.month.to_bytes(1, 'big') 42 | day = d.day.to_bytes(1, 'big') 43 | hour = d.hour.to_bytes(1, 'big') 44 | minute = d.minute.to_bytes(1, 'big') 45 | 46 | return is_active + is_action_turn_on + repeat_on_weekdays + year + month + day + hour + minute 47 | 48 | def encode(self, message): 49 | if isinstance(message, AuthorizeCommand): 50 | pin = self._encode_pin(message.pin) 51 | return self._encode_message(b'\x17\x00\x00' + pin + b'\x00\x00\x00\x00') 52 | 53 | if isinstance(message, ChangePinCommand): 54 | pin = self._encode_pin(message.pin) 55 | new_pin = self._encode_pin(message.new_pin) 56 | return self._encode_message(b'\x17\x00\x01' + new_pin + pin) 57 | 58 | if isinstance(message, ResetPinCommand): 59 | return self._encode_message(b'\x17\x00\x02' + b'\x00\x00\x00\x00\x00\x00\x00\x00') 60 | 61 | if isinstance(message, PowerSwitchCommand): 62 | if message.on: 63 | return self._encode_message(b'\x03\x00\x01' + b'\x00\x00') 64 | else: 65 | return self._encode_message(b'\x03\x00\x00' + b'\x00\x00') 66 | 67 | if isinstance(message, ChangeNightmodeCommand): 68 | if message.on: 69 | return self._encode_message(b'\x0f\x00\x05\x00' + b'\x00\x00\x00\x00') 70 | else: 71 | return self._encode_message(b'\x0f\x00\x05\x01' + b'\x00\x00\x00\x00') 72 | 73 | if isinstance(message, SynchronizeDateAndTimeCommand): 74 | d = datetime.datetime.fromisoformat(message.isodatetime) 75 | 76 | year = d.year.to_bytes(2, 'big') 77 | month = d.month.to_bytes(1, 'big') 78 | day = d.day.to_bytes(1, 'big') 79 | 80 | hour = d.hour.to_bytes(1, 'big') 81 | minute = d.minute.to_bytes(1, 'big') 82 | second = d.second.to_bytes(1, 'big') 83 | 84 | return self._encode_message(b'\x01\x00' + second + minute + hour + day + month + year + b'\x00\x00') 85 | 86 | if isinstance(message, RequestSettingsCommand): 87 | return self._encode_message(b'\x10\x00' + b'\x00\x00') 88 | 89 | if isinstance(message, ChangePowerLimitCommand): 90 | power_limit_in_watt = message.power_limit_in_watt.to_bytes(2, 'big') 91 | 92 | return self._encode_message(b'\x05\x00' + power_limit_in_watt + b'\x00\x00') 93 | 94 | if isinstance(message, ChangePricesCommand): 95 | normal_price_in_cent = message.normal_price_in_cent.to_bytes(1, 'big') 96 | reduced_period_price_in_cent = message.reduced_period_price_in_cent.to_bytes(1, 'big') 97 | 98 | return self._encode_message(b'\x0f\x00\x04' + normal_price_in_cent + reduced_period_price_in_cent + b'\x00\x00\x00\x00') 99 | 100 | if isinstance(message, ChangeReducedPeriodCommand): 101 | is_active = b'\x00' 102 | if message.is_active: 103 | is_active = b'\x01' 104 | 105 | start_time = datetime.time.fromisoformat(message.start_isotime) 106 | end_time = datetime.time.fromisoformat(message.end_isotime) 107 | 108 | start_time_in_minutes = (start_time.hour*60 + start_time.minute).to_bytes(2, 'big') 109 | end_time_in_minutes = (end_time.hour*60 + end_time.minute).to_bytes(2, 'big') 110 | 111 | return self._encode_message(b'\x0f\x00\x01' + is_active + start_time_in_minutes + end_time_in_minutes) 112 | 113 | if isinstance(message, RequestTimerStatusCommand): 114 | return self._encode_message(b'\x09\x00\x00' + b'\x00') 115 | 116 | if isinstance(message, SetTimerCommand): 117 | timer_action = b'\x00' 118 | if not message.is_reset_timer: 119 | timer_action = b'\x02' 120 | if message.is_action_turn_on: 121 | timer_action = b'\x01' 122 | 123 | target_second = b'\x00' 124 | target_minute = b'\x00' 125 | target_hour = b'\x00' 126 | target_day = b'\x00' 127 | target_month = b'\x00' 128 | target_year = b'\x00' 129 | 130 | if not message.target_isodatetime is None: 131 | d = datetime.datetime.fromisoformat(message.target_isodatetime) 132 | 133 | target_second = d.second.to_bytes(1, 'big') 134 | target_minute = d.minute.to_bytes(1, 'big') 135 | target_hour = d.hour.to_bytes(1, 'big') 136 | target_day = d.day.to_bytes(1, 'big') 137 | target_month = d.month.to_bytes(1, 'big') 138 | target_year = (d.year % 100).to_bytes(1, 'big') 139 | 140 | return self._encode_message(b'\x08\x00' + timer_action + target_second + target_minute + target_hour + target_day + target_month + target_year + b'\x00\x00') 141 | 142 | if isinstance(message, RequestSchedulerCommand): 143 | page_number = message.page_number.to_bytes(1, 'big') 144 | 145 | return self._encode_message(b'\x14\x00' + page_number + b'\x00\x00') 146 | 147 | if isinstance(message, AddSchedulerCommand): 148 | return self._encode_message(b'\x13\x00' + b'\x00\x00' + self._encode_scheduler(message.scheduler) + b'\x00\x00') 149 | 150 | if isinstance(message, EditSchedulerCommand): 151 | slot_id = message.slot_id.to_bytes(1, 'big') 152 | 153 | return self._encode_message(b'\x13\x00' + b'\x01' + slot_id + self._encode_scheduler(message.scheduler) + b'\x00\x00') 154 | 155 | if isinstance(message, RemoveSchedulerCommand): 156 | slot_id = message.slot_id.to_bytes(1, 'big') 157 | 158 | return self._encode_message(b'\x13\x00' + b'\x02' + slot_id + b'\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00') 159 | 160 | if isinstance(message, RequestRandomModeStatusCommand): 161 | return self._encode_message(b'\x16\x00' + b'\x00\x00') 162 | 163 | if isinstance(message, ChangeRandomModeCommand): 164 | is_active = b'\x00' 165 | if message.is_active: 166 | is_active = b'\x01' 167 | 168 | active_on_weekdays = 0 169 | for weekday in message.active_on_weekdays: 170 | active_on_weekdays += 2**weekday.value 171 | active_on_weekdays = active_on_weekdays.to_bytes(1, 'big') 172 | 173 | start_time = datetime.time.fromisoformat(message.start_isotime) 174 | end_time = datetime.time.fromisoformat(message.end_isotime) 175 | 176 | start_hour = start_time.hour.to_bytes(1, 'big') 177 | start_minute = start_time.minute.to_bytes(1, 'big') 178 | end_hour = end_time.hour.to_bytes(1, 'big') 179 | end_minute = end_time.minute.to_bytes(1, 'big') 180 | 181 | return self._encode_message(b'\x15\x00' + is_active + active_on_weekdays + start_hour + start_minute + end_hour + end_minute + b'\x00\x00') 182 | 183 | if isinstance(message, RequestMeasurementCommand): 184 | return self._encode_message(b'\x04\x00' + b'\x00\x00') 185 | 186 | if isinstance(message, RequestConsumptionOfLast12MonthsCommand): 187 | return self._encode_message(b'\x0c\x00' + b'\x00\x00') 188 | 189 | if isinstance(message, RequestConsumptionOfLast30DaysCommand): 190 | return self._encode_message(b'\x0b\x00' + b'\x00\x00') 191 | 192 | if isinstance(message, RequestConsumptionOfLast23HoursCommand): 193 | return self._encode_message(b'\x0a\x00' + b'\x00\x00') 194 | 195 | if isinstance(message, ResetConsumptionCommand): 196 | return self._encode_message(b'\x0f\x00' + b'\x02' + b'\x00\x00\x00\x00\x00') 197 | 198 | if isinstance(message, FactoryResetCommand): 199 | return self._encode_message(b'\x0f\x00' + b'\x00' + b'\x00\x00\x00\x00\x00') 200 | 201 | if isinstance(message, ChangeDeviceNameCommand): 202 | new_name = message.new_name 203 | if isinstance(new_name, str): 204 | new_name = new_name.encode() 205 | 206 | max_length = 20 207 | 208 | if len(new_name) > max_length: 209 | raise Exception('name is too long - actual number of character: ' + str(len(new_name)) + ', maximum characters possible: ' + str(max_length)) 210 | 211 | while len(new_name) < max_length: 212 | new_name += b'\x00' 213 | 214 | return self._encode_message(b'\x02\x00' + new_name) 215 | 216 | if isinstance(message, RequestDeviceSerialCommand): 217 | return self._encode_message(b'\x11\x00' + b'\x00\x00') 218 | 219 | if isinstance(message, AuthorizedNotification): 220 | was_successful = b'\x01' 221 | if message.was_successful: 222 | was_successful = b'\x00' 223 | 224 | return self._encode_message(b'\x17\x00' + was_successful + b'\x00\x00') 225 | 226 | if isinstance(message, PinChangedNotification): 227 | was_successful = b'\x01' 228 | if message.was_successful: 229 | was_successful = b'\x00' 230 | 231 | return self._encode_message(b'\x17\x00' + was_successful + b'\x01\x00') 232 | 233 | if isinstance(message, PinResetNotification): 234 | was_successful = b'\x01' 235 | if message.was_successful: 236 | was_successful = b'\x00' 237 | 238 | return self._encode_message(b'\x17\x00' + was_successful + b'\x02\x00') 239 | 240 | if isinstance(message, PowerSwitchedNotification): 241 | was_successful = b'\x01' 242 | if message.was_successful: 243 | was_successful = b'\x00' 244 | 245 | return self._encode_message(b'\x03\x00' + was_successful) 246 | 247 | if isinstance(message, NightmodeChangedNotification): 248 | return self._encode_message(b'\x0f\x00' + b'\x05\x00') 249 | 250 | if isinstance(message, DateAndTimeChangedNotification): 251 | was_successful = b'\x01' 252 | if message.was_successful: 253 | was_successful = b'\x00' 254 | 255 | return self._encode_message(b'\x01\x00' + was_successful) 256 | 257 | if isinstance(message, SettingsRequestedNotification): 258 | is_reduced_period = b'\x00' 259 | if message.is_reduced_period: 260 | is_reduced_period = b'\x01' 261 | 262 | normal_price_in_cent = message.normal_price_in_cent.to_bytes(1, 'big') 263 | reduced_period_price_in_cent = message.reduced_period_price_in_cent.to_bytes(1, 'big') 264 | 265 | reduced_period_start_time = datetime.time.fromisoformat(message.reduced_period_start_isotime) 266 | reduced_period_end_time = datetime.time.fromisoformat(message.reduced_period_end_isotime) 267 | 268 | reduced_period_start_time_in_minutes = (reduced_period_start_time.hour*60 + reduced_period_start_time.minute).to_bytes(2, 'big') 269 | reduced_period_end_time_in_minutes = (reduced_period_end_time.hour*60 + reduced_period_end_time.minute).to_bytes(2, 'big') 270 | 271 | is_nightmode_active = b'\x01' 272 | if message.is_nightmode_active: 273 | is_nightmode_active = b'\x00' 274 | 275 | power_limit_in_watt = message.power_limit_in_watt.to_bytes(2, 'big') 276 | 277 | return self._encode_message(b'\x10\x00' + is_reduced_period + normal_price_in_cent + reduced_period_price_in_cent + reduced_period_start_time_in_minutes + reduced_period_end_time_in_minutes + is_nightmode_active + b'\x00' + power_limit_in_watt) 278 | 279 | if isinstance(message, PowerLimitChangedNotification): 280 | return self._encode_message(b'\x05\x00' + b'\x00') 281 | 282 | if isinstance(message, PricesChangedNotification): 283 | return self._encode_message(b'\x0f\x00\x04' + b'\x00') 284 | 285 | if isinstance(message, ReducedPeriodChangedNotification): 286 | return self._encode_message(b'\x0f\x00\x01' + b'\x00') 287 | 288 | if isinstance(message, TimerStatusRequestedNotification): 289 | timer_action = b'\x00' 290 | if message.is_active: 291 | timer_action = b'\x02' 292 | if message.is_action_turn_on: 293 | timer_action = b'\x01' 294 | 295 | d = datetime.datetime.fromisoformat(message.target_isodatetime) 296 | 297 | target_second = d.second.to_bytes(1, 'big') 298 | target_minute = d.minute.to_bytes(1, 'big') 299 | target_hour = d.hour.to_bytes(1, 'big') 300 | target_day = d.day.to_bytes(1, 'big') 301 | target_month = d.month.to_bytes(1, 'big') 302 | target_year = (d.year % 100).to_bytes(1, 'big') 303 | 304 | original_timer_length_in_seconds = message.original_timer_length_in_seconds.to_bytes(3, 'big') 305 | 306 | return self._encode_message(b'\x09\x00' + timer_action + target_second + target_minute + target_hour + target_day + target_month + target_year + original_timer_length_in_seconds + b'\x00') 307 | 308 | if isinstance(message, TimerSetNotification): 309 | return self._encode_message(b'\x08\x00\x00') 310 | 311 | if isinstance(message, SchedulerRequestedNotification): 312 | schedulers_data = b'' 313 | 314 | number_of_schedulers = len(message.scheduler_entries) 315 | for i in range(number_of_schedulers): 316 | scheduler_entry = message.scheduler_entries[i] 317 | scheduler = scheduler_entry.scheduler 318 | 319 | slot_id = scheduler_entry.slot_id.to_bytes(1, 'big') 320 | 321 | scheduler_data = self._encode_scheduler(scheduler) + b'\x00\x00' 322 | checksum = (sum(scheduler_data)+0x14) & 0xff 323 | checksum = checksum.to_bytes(1, 'big') 324 | 325 | schedulers_data += slot_id + scheduler_data + checksum 326 | 327 | number_of_schedulers = number_of_schedulers.to_bytes(1, 'big') 328 | 329 | return self._encode_message(b'\x14\x00' + number_of_schedulers + schedulers_data) 330 | 331 | if isinstance(message, SchedulerChangedNotification): 332 | was_successful = b'\x01' 333 | if message.was_successful: 334 | was_successful = b'\x00' 335 | 336 | return self._encode_message(b'\x13\x00' + was_successful + b'\x00\x00') 337 | 338 | if isinstance(message, RandomModeStatusRequestedNotification): 339 | is_active = b'\x00' 340 | if message.is_active: 341 | is_active = b'\x01' 342 | 343 | active_on_weekdays = 0 344 | for weekday in message.active_on_weekdays: 345 | active_on_weekdays += 2**weekday.value 346 | active_on_weekdays = active_on_weekdays.to_bytes(1, 'big') 347 | 348 | start_time = datetime.time.fromisoformat(message.start_isotime) 349 | end_time = datetime.time.fromisoformat(message.end_isotime) 350 | 351 | start_hour = start_time.hour.to_bytes(1, 'big') 352 | start_minute = start_time.minute.to_bytes(1, 'big') 353 | end_hour = end_time.hour.to_bytes(1, 'big') 354 | end_minute = end_time.minute.to_bytes(1, 'big') 355 | 356 | return self._encode_message(b'\x16\x00' + is_active + active_on_weekdays + start_hour + start_minute + end_hour + end_minute + b'\x00\x00') 357 | 358 | if isinstance(message, RandomModeChangedNotification): 359 | was_successful = b'\x01' 360 | if message.was_successful: 361 | was_successful = b'\x00' 362 | 363 | return self._encode_message(b'\x15\x00' + was_successful + b'\x00') 364 | 365 | if isinstance(message, MeasurementRequestedNotification): 366 | is_power_active = b'\x00' 367 | if message.is_power_active: 368 | is_power_active = b'\x01' 369 | 370 | power_in_milliwatt = message.power_in_milliwatt.to_bytes(3, 'big') 371 | voltage_in_volt = message.voltage_in_volt.to_bytes(1, 'big') 372 | current_in_milliampere = message.current_in_milliampere.to_bytes(2, 'big') 373 | frequency_in_hertz = message.frequency_in_hertz.to_bytes(1, 'big') 374 | total_consumption_in_kilowatt_hour = message.total_consumption_in_kilowatt_hour.to_bytes(4, 'big') 375 | 376 | # suffix=b'\xff\xff' is missing in this notification 377 | return self._encode_message(b'\x04\x00' + is_power_active + power_in_milliwatt + voltage_in_volt + current_in_milliampere + frequency_in_hertz + b'\x00\x00' + total_consumption_in_kilowatt_hour, suffix=b'') 378 | 379 | if isinstance(message, ConsumptionOfLast12MonthsRequestedNotification): 380 | consumptions = b'' 381 | 382 | # notification does not contain measurements for current months 383 | for i in range(1, len(message.consumption_n_months_ago_in_watt_hour)): 384 | consumption = message.consumption_n_months_ago_in_watt_hour[i] 385 | consumptions = consumption.to_bytes(3, 'big') + b'\x00' + consumptions 386 | 387 | return self._encode_message(b'\x0c\x00' + consumptions) 388 | 389 | if isinstance(message, ConsumptionOfLast30DaysRequestedNotification): 390 | consumptions = b'' 391 | 392 | # notification does not contain measurements for today 393 | for i in range(1, len(message.consumption_n_days_ago_in_watt_hour)): 394 | consumption = message.consumption_n_days_ago_in_watt_hour[i] 395 | consumptions = consumption.to_bytes(3, 'big') + b'\x00' + consumptions 396 | 397 | return self._encode_message(b'\x0b\x00' + consumptions) 398 | 399 | if isinstance(message, ConsumptionOfLast23HoursRequestedNotification): 400 | consumptions = b'' 401 | 402 | for consumption in message.consumption_n_hours_ago_in_watt_hour: 403 | consumptions = consumption.to_bytes(2, 'big') + consumptions 404 | 405 | return self._encode_message(b'\x0a\x00' + consumptions) 406 | 407 | if isinstance(message, ConsumptionResetNotification): 408 | return self._encode_message(b'\x0f\x00' + b'\x02' + b'\x00') 409 | 410 | if isinstance(message, FactoryResetNotification): 411 | return self._encode_message(b'\x0f\x00' + b'\x00' + b'\x00') 412 | 413 | if isinstance(message, DeviceNameChangedNotification): 414 | return self._encode_message(b'\x02\x00' + b'\x00') 415 | 416 | if isinstance(message, DeviceSerialRequestedNotification): 417 | serial = message.serial.encode() 418 | 419 | return self._encode_message(b'\x11\x00' + serial + b'\x00\x00') 420 | 421 | raise Exception('Unsupported message ' + str(message)) 422 | 423 | -------------------------------------------------------------------------------- /sem6000/message.py: -------------------------------------------------------------------------------- 1 | from . import util 2 | 3 | import datetime 4 | 5 | class AbstractCommand: 6 | def __str__(self): 7 | name = self.__class__.__name__ 8 | return name + "()" 9 | 10 | 11 | class AbstractSwitchCommand(): 12 | def __init__(self, on): 13 | self.on = on 14 | 15 | def __str__(self): 16 | name = self.__class__.__name__ 17 | return name + "(on=" + str(self.on) + ")" 18 | 19 | 20 | class AbstractCommandConfirmationNotification: 21 | def __init__(self, was_successful): 22 | self.was_successful = was_successful 23 | 24 | def __str__(self): 25 | name = self.__class__.__name__ 26 | return name + "(was_successful=" + str(self.was_successful) + ")" 27 | 28 | 29 | class AuthorizeCommand(): 30 | def __init__(self, pin): 31 | self.pin = pin 32 | 33 | def __str__(self): 34 | name = self.__class__.__name__ 35 | return name + "(pin=" + str(self.pin) + ")" 36 | 37 | 38 | class ChangePinCommand(): 39 | def __init__(self, pin, new_pin): 40 | self.pin = pin 41 | self.new_pin = new_pin 42 | 43 | def __str__(self): 44 | name = self.__class__.__name__ 45 | return name + "(pin=" + str(self.pin) + ", new_pin=" + str(self.new_pin) + ")" 46 | 47 | 48 | class ResetPinCommand(AbstractCommand): 49 | pass 50 | 51 | 52 | class PowerSwitchCommand(AbstractSwitchCommand): 53 | pass 54 | 55 | 56 | class ChangeNightmodeCommand(AbstractSwitchCommand): 57 | pass 58 | 59 | 60 | class SynchronizeDateAndTimeCommand(): 61 | def __init__(self, isodatetime): 62 | d = datetime.datetime.fromisoformat(isodatetime) 63 | self.isodatetime = d.isoformat(timespec='seconds') 64 | 65 | def __str__(self): 66 | name = self.__class__.__name__ 67 | return name + "(isodatetime=" + str(self.isodatetime) + ")" 68 | 69 | 70 | class RequestSettingsCommand(AbstractCommand): 71 | pass 72 | 73 | 74 | class ChangePowerLimitCommand(): 75 | def __init__(self, power_limit_in_watt): 76 | self.power_limit_in_watt = power_limit_in_watt 77 | 78 | def __str__(self): 79 | name = self.__class__.__name__ 80 | return name + "(power_limit_in_watt=" + str(self.power_limit_in_watt) + ")" 81 | 82 | 83 | class ChangePricesCommand(): 84 | def __init__(self, normal_price_in_cent, reduced_period_price_in_cent): 85 | self.normal_price_in_cent = normal_price_in_cent 86 | self.reduced_period_price_in_cent = reduced_period_price_in_cent 87 | 88 | def __str__(self): 89 | name = self.__class__.__name__ 90 | return name + "(normal_price_in_cent=" + str(self.normal_price_in_cent) + ", reduced_period_price_in_cent=" + str(self.reduced_period_price_in_cent) + ")" 91 | 92 | 93 | class ChangeReducedPeriodCommand(): 94 | def __init__(self, is_active, start_isotime, end_isotime): 95 | self.is_active = is_active 96 | self.start_isotime = start_isotime 97 | self.end_isotime = end_isotime 98 | 99 | def __str__(self): 100 | name = self.__class__.__name__ 101 | return name + "(is_active=" + str(self.is_active) + ", start_isotime=" + str(self.start_isotime) + ", end_isotime=" + str(self.end_isotime) + ")" 102 | 103 | 104 | class RequestTimerStatusCommand(AbstractCommand): 105 | pass 106 | 107 | 108 | class SetTimerCommand: 109 | def __init__(self, is_reset_timer, is_action_turn_on, target_isodatetime=None): 110 | if not is_reset_timer and target_isodatetime is None: 111 | raise Exception("target_isodatetime parameter is None") 112 | 113 | if is_reset_timer and not target_isodatetime is None: 114 | raise Exception("target_isodatetime parameter is expected to be None if timer is being reset") 115 | 116 | self.is_reset_timer = is_reset_timer 117 | self.is_action_turn_on = is_action_turn_on 118 | self.target_isodatetime = None 119 | 120 | if not is_reset_timer: 121 | d = datetime.datetime.fromisoformat(target_isodatetime) 122 | self.target_isodatetime = d.isoformat(timespec='seconds') 123 | 124 | def __str__(self): 125 | name = self.__class__.__name__ 126 | return name + "(is_reset_timer=" + str(self.is_reset_timer) + ", is_action_turn_on=" + str(self.is_action_turn_on) + ", target_isodatetime=" + str(self.target_isodatetime) + ")" 127 | 128 | 129 | class RequestSchedulerCommand: 130 | def __init__(self, page_number): 131 | self.page_number = page_number 132 | 133 | def __str__(self): 134 | name = self.__class__.__name__ 135 | return name + "(page_number=" + str(self.page_number) + ")" 136 | 137 | 138 | class AddSchedulerCommand: 139 | def __init__(self, scheduler): 140 | assert isinstance(scheduler, Scheduler) 141 | 142 | self.scheduler = scheduler 143 | 144 | def __str__(self): 145 | name = self.__class__.__name__ 146 | return name + "(scheduler=" + str(self.scheduler) + ")" 147 | 148 | 149 | class EditSchedulerCommand: 150 | def __init__(self, slot_id, scheduler): 151 | assert isinstance(scheduler, Scheduler) 152 | 153 | self.slot_id = slot_id 154 | self.scheduler = scheduler 155 | 156 | def __str__(self): 157 | name = self.__class__.__name__ 158 | return name + "(slot_id=" + str(self.slot_id) + ", scheduler=" + str(self.scheduler) + ")" 159 | 160 | 161 | class RemoveSchedulerCommand: 162 | def __init__(self, slot_id): 163 | self.slot_id = slot_id 164 | 165 | def __str__(self): 166 | name = self.__class__.__name__ 167 | return name + "(slot_id=" + str(self.slot_id) + ")" 168 | 169 | 170 | class RequestRandomModeStatusCommand(AbstractCommand): 171 | pass 172 | 173 | 174 | class ChangeRandomModeCommand: 175 | def __init__(self, is_active, active_on_weekdays, start_isotime, end_isotime): 176 | active_on_weekdays = util._list_values_to_enum(util.Weekday, active_on_weekdays) 177 | 178 | self.is_active = is_active 179 | self.active_on_weekdays = active_on_weekdays 180 | 181 | start_time = datetime.time.fromisoformat(start_isotime) 182 | end_time = datetime.time.fromisoformat(end_isotime) 183 | 184 | self.start_isotime = start_time.isoformat(timespec='minutes') 185 | self.end_isotime = end_time.isoformat(timespec='minutes') 186 | 187 | def __str__(self): 188 | weekday_formatter = lambda w: w.name 189 | active_on_weekdays = util._format_list_of_objects(weekday_formatter, self.active_on_weekdays) 190 | 191 | name = self.__class__.__name__ 192 | return name + "(is_active=" + str(self.is_active) + ", active_on_weekdays=" + active_on_weekdays + ", start_isotime=" + str(self.start_isotime) + ", end_isotime=" + str(self.end_isotime) + ")" 193 | 194 | 195 | class RequestMeasurementCommand(AbstractCommand): 196 | pass 197 | 198 | 199 | class RequestConsumptionOfLast12MonthsCommand(AbstractCommand): 200 | pass 201 | 202 | 203 | class RequestConsumptionOfLast30DaysCommand(AbstractCommand): 204 | pass 205 | 206 | 207 | class RequestConsumptionOfLast23HoursCommand(AbstractCommand): 208 | pass 209 | 210 | 211 | class ResetConsumptionCommand(AbstractCommand): 212 | pass 213 | 214 | 215 | class FactoryResetCommand(AbstractCommand): 216 | pass 217 | 218 | 219 | class ChangeDeviceNameCommand: 220 | def __init__(self, new_name): 221 | self.new_name = new_name 222 | 223 | def __str__(self): 224 | command = self.__class__.__name__ 225 | return command + "(new_name=" + str(self.new_name) + ")" 226 | 227 | 228 | class RequestDeviceSerialCommand(AbstractCommand): 229 | pass 230 | 231 | 232 | class AuthorizedNotification(AbstractCommandConfirmationNotification): 233 | pass 234 | 235 | 236 | class PinChangedNotification(AbstractCommandConfirmationNotification): 237 | pass 238 | 239 | 240 | class PinResetNotification(AbstractCommandConfirmationNotification): 241 | pass 242 | 243 | 244 | class PowerSwitchedNotification(AbstractCommandConfirmationNotification): 245 | pass 246 | 247 | 248 | class NightmodeChangedNotification(AbstractCommandConfirmationNotification): 249 | pass 250 | 251 | 252 | class DateAndTimeChangedNotification(AbstractCommandConfirmationNotification): 253 | pass 254 | 255 | 256 | class SettingsRequestedNotification: 257 | def __init__(self, is_reduced_period, normal_price_in_cent, reduced_period_price_in_cent, reduced_period_start_isotime, reduced_period_end_isotime, is_nightmode_active, power_limit_in_watt): 258 | self.is_reduced_period = is_reduced_period 259 | self.normal_price_in_cent = normal_price_in_cent 260 | self.reduced_period_price_in_cent = reduced_period_price_in_cent 261 | 262 | start_time = datetime.time.fromisoformat(reduced_period_start_isotime) 263 | end_time = datetime.time.fromisoformat(reduced_period_end_isotime) 264 | 265 | self.reduced_period_start_isotime = start_time.isoformat(timespec='minutes') 266 | self.reduced_period_end_isotime = end_time.isoformat(timespec='minutes') 267 | 268 | self.is_nightmode_active = is_nightmode_active 269 | self.power_limit_in_watt = power_limit_in_watt 270 | 271 | def __str__(self): 272 | name = self.__class__.__name__ 273 | return name + "(is_reduced_period=" + str(self.is_reduced_period) + ", normal_price_in_cent=" + str(self.normal_price_in_cent) + ", reduced_periiod_price_in_cent=" + str(self.reduced_period_price_in_cent) + ", reduced_period_start_isotime=" + str(self.reduced_period_start_isotime) + ", reduced_period_end_isotime=" + str(self.reduced_period_end_isotime) + ", is_nightmode_active=" + str(self.is_nightmode_active) + ", power_limit_in_watt=" + str(self.power_limit_in_watt) + ")" 274 | 275 | 276 | class PowerLimitChangedNotification(AbstractCommandConfirmationNotification): 277 | pass 278 | 279 | 280 | class PricesChangedNotification(AbstractCommandConfirmationNotification): 281 | pass 282 | 283 | 284 | class ReducedPeriodChangedNotification(AbstractCommandConfirmationNotification): 285 | pass 286 | 287 | 288 | class TimerStatusRequestedNotification: 289 | def __init__(self, is_active, is_action_turn_on, target_isodatetime, original_timer_length_in_seconds): 290 | d = datetime.datetime.fromisoformat(target_isodatetime) 291 | 292 | self.is_active = is_active 293 | self.is_action_turn_on = is_action_turn_on 294 | self.target_isodatetime = d.isoformat(timespec='seconds') 295 | self.original_timer_length_in_seconds = original_timer_length_in_seconds 296 | 297 | def __str__(self): 298 | name = self.__class__.__name__ 299 | return name + "(is_active=" + str(self.is_active) + ", is_action_turn_on=" + str(self.is_action_turn_on) + ", target_isodatetime=" + str(self.target_isodatetime) + ", original_timer_length_in_seconds=" + str(self.original_timer_length_in_seconds) + ")" 300 | 301 | 302 | class TimerSetNotification(AbstractCommandConfirmationNotification): 303 | pass 304 | 305 | 306 | class Scheduler: 307 | def __init__(self, is_active, is_action_turn_on, repeat_on_weekdays, isodatetime): 308 | 309 | repeat_on_weekdays = util._list_values_to_enum(util.Weekday, repeat_on_weekdays) 310 | 311 | self.is_active = is_active 312 | self.is_action_turn_on = is_action_turn_on 313 | self.repeat_on_weekdays = repeat_on_weekdays 314 | 315 | d = datetime.datetime.fromisoformat(isodatetime) 316 | self.isodatetime = d.isoformat(timespec='minutes') 317 | 318 | 319 | class OneTimeScheduler(Scheduler): 320 | def __init__(self, is_active, is_action_turn_on, isodatetime): 321 | Scheduler.__init__(self, 322 | is_active=is_active, 323 | is_action_turn_on=is_action_turn_on, 324 | repeat_on_weekdays=[], 325 | isodatetime=isodatetime) 326 | 327 | def __str__(self): 328 | name = self.__class__.__name__ 329 | 330 | weekday_formatter = lambda w: w.name 331 | repeat_on_weekdays = util._format_list_of_objects(weekday_formatter, self.repeat_on_weekdays) 332 | 333 | return name + "(is_active=" + str(self.is_active) + ", is_action_turn_on=" + str(self.is_action_turn_on) + ", isodatetime=" + str(self.isodatetime) + ")" 334 | 335 | 336 | class RepeatedScheduler(Scheduler): 337 | def __init__(self, is_active, is_action_turn_on, repeat_on_weekdays, isotime): 338 | Scheduler.__init__(self, 339 | is_active=is_active, 340 | is_action_turn_on=is_action_turn_on, 341 | repeat_on_weekdays=repeat_on_weekdays, 342 | isodatetime=datetime.date.today().isoformat() + 'T' + isotime) 343 | 344 | def __str__(self): 345 | name = self.__class__.__name__ 346 | 347 | weekday_formatter = lambda w: w.name 348 | repeat_on_weekdays = util._format_list_of_objects(weekday_formatter, self.repeat_on_weekdays) 349 | 350 | isotime = datetime.datetime.fromisoformat(self.isodatetime).time().isoformat(timespec='minutes') 351 | 352 | return name + "(is_active=" + str(self.is_active) + ", is_action_turn_on=" + str(self.is_action_turn_on) + ", repeat_on_weekdays=" + repeat_on_weekdays + ", isotime=" + isotime + ")" 353 | 354 | 355 | class SchedulerEntry: 356 | def __init__(self, slot_id, scheduler): 357 | assert isinstance(scheduler, Scheduler) 358 | 359 | self.slot_id = slot_id 360 | self.scheduler = scheduler 361 | 362 | def __str__(self): 363 | name = self.__class__.__name__ 364 | return name + "(slot_id=" + str(self.slot_id) + ", scheduler=" + str(self.scheduler) + ")" 365 | 366 | 367 | class SchedulerRequestedNotification: 368 | def __init__(self, number_of_schedulers, scheduler_entries): 369 | for scheduler_entry in scheduler_entries: 370 | assert isinstance(scheduler_entry, SchedulerEntry) 371 | 372 | self.number_of_schedulers = number_of_schedulers 373 | self.scheduler_entries = scheduler_entries 374 | 375 | def __str__(self): 376 | name = self.__class__.__name__ 377 | 378 | scheduler_entries = util._format_list_of_objects(str, self.scheduler_entries) 379 | 380 | return name + "(number_of_schedulers=" + str(self.number_of_schedulers) + ", scheduler_entries=" + scheduler_entries + ")" 381 | 382 | 383 | class SchedulerChangedNotification(AbstractCommandConfirmationNotification): 384 | pass 385 | 386 | 387 | class RandomModeStatusRequestedNotification: 388 | def __init__(self, is_active, active_on_weekdays, start_isotime, end_isotime): 389 | active_on_weekdays = util._list_values_to_enum(util.Weekday, active_on_weekdays) 390 | 391 | self.is_active = is_active 392 | self.active_on_weekdays = active_on_weekdays 393 | 394 | start_time = datetime.time.fromisoformat(start_isotime) 395 | end_time = datetime.time.fromisoformat(end_isotime) 396 | 397 | self.start_isotime = start_time.isoformat(timespec='minutes') 398 | self.end_isotime = end_time.isoformat(timespec='minutes') 399 | 400 | def __str__(self): 401 | weekday_formatter = lambda w: w.name 402 | active_on_weekdays = util._format_list_of_objects(weekday_formatter, self.active_on_weekdays) 403 | 404 | name = self.__class__.__name__ 405 | return name + "(is_active=" + str(self.is_active) + ", active_on_weekdays=" + active_on_weekdays + ", start_isotime=" + str(self.start_isotime) + ", end_isotime=" + str(self.end_isotime) + ")" 406 | 407 | 408 | class RandomModeChangedNotification(AbstractCommandConfirmationNotification): 409 | pass 410 | 411 | 412 | class MeasurementRequestedNotification: 413 | def __init__(self, is_power_active, power_in_milliwatt, voltage_in_volt, current_in_milliampere, frequency_in_hertz, total_consumption_in_kilowatt_hour): 414 | self.is_power_active = is_power_active 415 | self.power_in_milliwatt = power_in_milliwatt 416 | self.voltage_in_volt = voltage_in_volt 417 | self.current_in_milliampere = current_in_milliampere 418 | self.frequency_in_hertz = frequency_in_hertz 419 | self.total_consumption_in_kilowatt_hour = total_consumption_in_kilowatt_hour 420 | 421 | def __str__(self): 422 | name = self.__class__.__name__ 423 | return name + "(is_power_active=" + str(self.is_power_active) + ", power_in_milliwatt=" + str(self.power_in_milliwatt) + ", voltage_in_volt=" + str(self.voltage_in_volt) + ", current_in_milliampere=" + str(self.current_in_milliampere) + ", frequency_in_hertz=" + str(self.frequency_in_hertz) + ", total_consumption_in_kilowatt_hour=" + str(self.total_consumption_in_kilowatt_hour) + ")" 424 | 425 | 426 | class ConsumptionOfLast12MonthsRequestedNotification: 427 | def __init__(self, consumption_n_months_ago_in_watt_hour): 428 | self.consumption_n_months_ago_in_watt_hour = consumption_n_months_ago_in_watt_hour 429 | 430 | def __str__(self): 431 | name = self.__class__.__name__ 432 | return name + "(consumption_n_months_ago_in_watt_hour=" + util._format_list_of_objects(str, self.consumption_n_months_ago_in_watt_hour) + ")" 433 | 434 | 435 | class ConsumptionOfLast30DaysRequestedNotification: 436 | def __init__(self, consumption_n_days_ago_in_watt_hour): 437 | self.consumption_n_days_ago_in_watt_hour = consumption_n_days_ago_in_watt_hour 438 | 439 | def __str__(self): 440 | name = self.__class__.__name__ 441 | return name + "(consumption_n_days_ago_in_watt_hour=" + util._format_list_of_objects(str, self.consumption_n_days_ago_in_watt_hour) + ")" 442 | 443 | 444 | class ConsumptionOfLast23HoursRequestedNotification: 445 | def __init__(self, consumption_n_hours_ago_in_watt_hour): 446 | self.consumption_n_hours_ago_in_watt_hour = consumption_n_hours_ago_in_watt_hour 447 | 448 | def __str__(self): 449 | name = self.__class__.__name__ 450 | return name + "(consumption_n_hours_ago_in_watt_hour=" + util._format_list_of_objects(str, self.consumption_n_hours_ago_in_watt_hour) + ")" 451 | 452 | 453 | class ConsumptionResetNotification(AbstractCommandConfirmationNotification): 454 | pass 455 | 456 | 457 | class FactoryResetNotification(AbstractCommandConfirmationNotification): 458 | pass 459 | 460 | 461 | class DeviceNameChangedNotification(AbstractCommandConfirmationNotification): 462 | pass 463 | 464 | 465 | class DeviceNameRequestedNotification: 466 | def __init__(self, device_name): 467 | self.device_name = device_name 468 | 469 | def __str__(self): 470 | name = self.__class__.name 471 | return name + "(device_name=" + self.device_name + ")" 472 | 473 | 474 | class DeviceSerialRequestedNotification: 475 | def __init__(self, serial): 476 | self.serial = serial 477 | 478 | def __str__(self): 479 | name = self.__class__.__name__ 480 | return name + "(serial=" + str(self.serial) + ")" 481 | 482 | -------------------------------------------------------------------------------- /sem6000/parser.py: -------------------------------------------------------------------------------- 1 | from .message import * 2 | from . import util 3 | 4 | import datetime 5 | import sys 6 | 7 | class InvalidPayloadLengthException(Exception): 8 | def __init__(self, message_class, expected_payload_length, actual_payload_length): 9 | self.message_class = message_class 10 | self.expected_payload_length = expected_payload_length 11 | self.actual_payload_length = actual_payload_length 12 | 13 | def __str__(self): 14 | return "message has invalid payload length for " + self.message_class.__name__ + " (expected: " + str(self.expected_payload_length) + ", actual=" + str(self.actual_payload_length) + ")" 15 | 16 | class MessageParser: 17 | def __init__(self, hardware_version=None, year_diff=None): 18 | self.hardware_version = hardware_version 19 | 20 | # the device only operates with two digit years 21 | # determine or set the difference to the current 4 digit year 22 | if year_diff is None: 23 | self.year_diff = (datetime.datetime.now().year // 100) * 100 24 | else: 25 | self.year_diff = year_diff 26 | 27 | def _parse_payload(self, data): 28 | if data[0:1] != b'\x0f': 29 | raise Exception("Invalid response") 30 | 31 | length_of_payload = data[1] 32 | 33 | # In hardware version 3 the "capture measurement"-notification might contain a 34 | # payload length that is 2 bytes too short 35 | if (len(data) >= 4 and data[2:4] == b'\x04\x00') and self.hardware_version == 3: 36 | length_of_payload += 2 37 | 38 | payload = data[2:2+length_of_payload-1] 39 | checksum_received = data[2+length_of_payload-1] 40 | 41 | checksum = (1+sum(payload)) & 0xff 42 | 43 | if checksum_received != checksum: 44 | raise Exception("Invalid checksum: actual=" + str(checksum) + ", received=" + str(checksum_received)) 45 | 46 | if len(data) > 2+length_of_payload: 47 | # if suffix of payload exists it must be b'\xff\xff' 48 | 49 | # in hardware version >= 3 message b'\x10\x00' (SettingsRequestedNotification) 50 | # has two extra bytes behind b'\xff\xff' 51 | # -> so only the next two bytes directly following the payload are checked here 52 | 53 | suffix = data[2+length_of_payload:4+length_of_payload] 54 | if suffix != b'\xff\xff': 55 | raise Exception("Invalid suffix " + str(suffix)) 56 | 57 | return payload 58 | 59 | def _parse_scheduler(self, data): 60 | is_active = False 61 | if data[0:1] == b'\x01': 62 | is_active = True 63 | 64 | is_action_turn_on = False 65 | if data[1:2] == b'\x01': 66 | is_action_turn_on = True 67 | 68 | repeat_on_weekdays_mask = int.from_bytes(data[2:3], 'big') 69 | repeat_on_weekdays = [] 70 | for w in range(7): 71 | if repeat_on_weekdays_mask & 2**w: 72 | repeat_on_weekdays.append(w) 73 | 74 | # only the last two digits are returned for the year 75 | year = int.from_bytes(data[3:4], 'big') + self.year_diff 76 | month = int.from_bytes(data[4:5], 'big') 77 | day = int.from_bytes(data[5:6], 'big') 78 | hour = int.from_bytes(data[6:7], 'big') 79 | minute = int.from_bytes(data[7:8], 'big') 80 | 81 | if len(repeat_on_weekdays): 82 | t = datetime.time(hour, minute) 83 | 84 | return RepeatedScheduler( 85 | is_active=is_active, 86 | is_action_turn_on=is_action_turn_on, 87 | repeat_on_weekdays=repeat_on_weekdays, 88 | isotime=t.isoformat(timespec='minutes')) 89 | else: 90 | d = datetime.datetime(year, month, day, hour, minute) 91 | 92 | return OneTimeScheduler( 93 | is_active=is_active, 94 | is_action_turn_on=is_action_turn_on, 95 | isodatetime=d.isoformat(timespec='minutes')) 96 | 97 | def parse(self, data): 98 | payload = self._parse_payload(data) 99 | 100 | if payload[0:2] == b'\x17\x00' and payload[3:4] == b'\x00': 101 | if len(payload) != 5: 102 | raise InvalidPayloadLengthException(message_class=AuthenticationNotification.__class__, expected_payload_length=5, actual_payload_length=len(payload)) 103 | 104 | was_successful = False 105 | if payload[2:3] == b'\x00': 106 | was_successful = True 107 | 108 | return AuthorizedNotification(was_successful=was_successful) 109 | 110 | if payload[0:2] == b'\x17\x00' and payload[3:4] == b'\x01': 111 | if len(payload) != 5: 112 | raise InvalidPayloadLengthException(message_class=PinChangedNotification.__class__, expected_payload_length=5, actual_payload_length=len(payload)) 113 | 114 | was_successful = False 115 | if payload[2:3] == b'\x00': 116 | was_successful = True 117 | 118 | return PinChangedNotification(was_successful=was_successful) 119 | 120 | if payload[0:2] == b'\x17\x00' and payload[3:4] == b'\x02': 121 | if len(payload) != 5: 122 | raise InvalidPayloadLengthException(message_class=PinResetNotification.__class__, expected_payload_length=5, actual_payload_length=len(payload)) 123 | 124 | was_successful = False 125 | if payload[2:3] == b'\x00': 126 | was_successful = True 127 | 128 | return PinResetNotification(was_successful=was_successful) 129 | 130 | if payload[0:2] == b'\x03\x00': 131 | if len(payload) != 3: 132 | raise InvalidPayloadLengthException(message_class=PowerSwitchedNotification.__class__, expected_payload_length=3, actual_payload_length=len(payload)) 133 | 134 | was_successful = False 135 | if payload[2:3] == b'\x00': 136 | was_successful = True 137 | 138 | return PowerSwitchedNotification(was_successful=was_successful) 139 | 140 | if payload[0:3] == b'\x0f\x00\x05': 141 | if len(payload) != 4: 142 | raise InvalidPayloadLengthException(message_class=NightmodeChangedNotification.__class__, expected_payload_length=4, actual_payload_length=len(payload)) 143 | 144 | return NightmodeChangedNotification(was_successful=True) 145 | 146 | if payload[0:2] == b'\x01\x00': 147 | if len(payload) != 3: 148 | raise InvalidPayloadLengthException(message_class=DateAndTimeChangedNotification.__class__, expected_payload_length=3, actual_payload_length=len(payload)) 149 | 150 | was_successful = False 151 | if payload[2:3] == b'\x00': 152 | was_successful = True 153 | 154 | return DateAndTimeChangedNotification(was_successful=was_successful) 155 | 156 | if payload[0:2] == b'\x10\x00': 157 | if len(payload) != 13: 158 | raise InvalidPayloadLengthException(message_class=SettingsRequestedNotification.__class__, expected_payload_length=13, actual_payload_length=len(payload)) 159 | 160 | is_reduced_period = False 161 | if payload[2:3] == b'\x01': 162 | is_reduced_period = True 163 | 164 | normal_price_in_cent = int.from_bytes(payload[3:4], 'big') 165 | reduced_period_price_in_cent = int.from_bytes(payload[4:5], 'big') 166 | 167 | reduced_period_start_time_in_minutes = int.from_bytes(payload[5:7], 'big') 168 | reduced_period_end_time_in_minutes = int.from_bytes(payload[7:9], 'big') 169 | 170 | reduced_period_start_time = util._parse_time_from_minutes(reduced_period_start_time_in_minutes) 171 | reduced_period_end_time = util._parse_time_from_minutes(reduced_period_end_time_in_minutes) 172 | 173 | is_nightmode_active = True 174 | if payload[9:10] == b'\x01': 175 | is_nightmode_active = False 176 | 177 | power_limit_in_watt = int.from_bytes(payload[11:13], 'big') 178 | 179 | return SettingsRequestedNotification(is_reduced_period=is_reduced_period, normal_price_in_cent=normal_price_in_cent, reduced_period_price_in_cent=reduced_period_price_in_cent, reduced_period_start_isotime=reduced_period_start_time.isoformat(timespec='minutes'), reduced_period_end_isotime=reduced_period_end_time.isoformat('minutes'), is_nightmode_active=is_nightmode_active, power_limit_in_watt=power_limit_in_watt) 180 | 181 | if payload[0:3] == b'\x05\x00\x00' and len(payload) == 3: 182 | return PowerLimitChangedNotification(was_successful=True) 183 | 184 | if payload[0:3] == b'\x0f\x00\x04': 185 | if len(payload) != 4: 186 | raise InvalidPayloadLengthException(message_class=PricesChangedNotification.__class__, expected_payload_length=4, actual_payload_length=len(payload)) 187 | 188 | return PricesChangedNotification(was_successful=True) 189 | 190 | if payload[0:3] == b'\x0f\x00\x01': 191 | if len(payload) != 4: 192 | raise InvalidPayloadLengthException(message_class=ReducedPeriodChangedNotification.__class__, expected_payload_length=4, actual_payload_length=len(payload)) 193 | 194 | return ReducedPeriodChangedNotification(was_successful=True) 195 | 196 | if payload[0:2] == b'\x09\x00': 197 | if len(payload) != 13: 198 | raise InvalidPayloadLengthException(message_class=TimerStatusRequestedNotification.__class__, expected_payload_length=13, actual_payload_length=len(payload)) 199 | 200 | is_active = False 201 | is_action_turn_on = False 202 | 203 | if payload[2:3] == b'\x01': 204 | is_active = True 205 | is_action_turn_on = True 206 | if payload[2:3] == b'\x02': 207 | is_active = True 208 | 209 | target_second = payload[3] 210 | target_minute = payload[4] 211 | target_hour = payload[5] 212 | target_day = payload[6] 213 | target_month = payload[7] 214 | # only the last two digits are returned for the year 215 | target_year = payload[8] + self.year_diff 216 | 217 | original_timer_length_in_seconds = int.from_bytes(payload[9:12], 'big') 218 | 219 | if target_year and target_month and target_day: 220 | d = datetime.datetime(target_year, target_month, target_day, target_hour, target_minute, target_second) 221 | else: 222 | d = datetime.datetime(1970, 1, 1, target_hour, target_minute, target_second) 223 | 224 | return TimerStatusRequestedNotification(is_active=is_active, is_action_turn_on=is_action_turn_on, target_isodatetime=d.isoformat(timespec='seconds'), original_timer_length_in_seconds=original_timer_length_in_seconds) 225 | 226 | if payload[0:2] == b'\x08\x00': 227 | if len(payload) != 3: 228 | raise InvalidPayloadLengthException(message_class=TimerSetNotification, expected_payload_length=3, actual_payload_length=len(payload)) 229 | 230 | return TimerSetNotification(was_successful=True) 231 | 232 | if payload[0:2] == b'\x14\x00': 233 | if len(payload) < 3: 234 | raise InvalidPayloadLengthException(message_class=SchedulerRequestedNotification, expected_payload_length=3, actual_payload_length=len(payload)) 235 | if (len(payload)-3) % 12 != 0: 236 | expected = len(payload) + 12 - (len(payload)-3) % 12 237 | raise InvalidPayloadLengthException(message_class=SchedulerRequestedNotification, expected_payload_length=expected, actual_payload_length=len(payload)) 238 | 239 | number_of_schedulers = int.from_bytes(payload[2:3], 'big') 240 | number_of_schedulers_in_message = (len(payload)-3)//12 241 | 242 | scheduler_entries = [] 243 | for i in range(number_of_schedulers_in_message): 244 | slot_id = int.from_bytes(payload[3 + i*12:4 + i*12], 'big') 245 | 246 | checksum_received = int.from_bytes(payload[14 + i*12:15 + i*12], 'big') 247 | checksum = (sum(payload[4 + i*12:14 + i*12])+0x14) & 0xff 248 | 249 | if checksum_received != checksum: 250 | # TODO: how to calculate the correct checksum? 251 | print("Invalid checksum for scheduler " + str(slot_id) + ": actual=" + str(checksum) + ", received=" + str(checksum_received), file=sys.stderr) 252 | # raise Exception("Invalid checksum for scheduler " + str(slot_id) + ": actual=" + str(checksum) + ", received=" + str(checksum_received)) 253 | 254 | scheduler = self._parse_scheduler(payload[4 + i*12:12 + i*12]) 255 | 256 | scheduler_entries.append(SchedulerEntry(slot_id=slot_id, scheduler=scheduler)) 257 | 258 | return SchedulerRequestedNotification(number_of_schedulers=number_of_schedulers, scheduler_entries=scheduler_entries) 259 | 260 | if payload[0:2] == b'\x13\x00': 261 | was_successful = False 262 | if payload[2:3] == b'\x00': 263 | was_successful = True 264 | 265 | return SchedulerChangedNotification(was_successful=was_successful) 266 | 267 | if payload[0:2] == b'\x16\x00': 268 | is_active = False 269 | if payload[2:3] == b'\x01': 270 | is_active = True 271 | 272 | active_on_weekdays_mask = int.from_bytes(payload[3:4], 'big') 273 | active_on_weekdays = [] 274 | for w in range(7): 275 | if active_on_weekdays_mask & 2**w: 276 | active_on_weekdays.append(w) 277 | 278 | start_hour = int.from_bytes(payload[4:5], 'big') 279 | start_minute = int.from_bytes(payload[5:6], 'big') 280 | end_hour = int.from_bytes(payload[6:7], 'big') 281 | end_minute = int.from_bytes(payload[7:8], 'big') 282 | 283 | start_time = datetime.time(start_hour, start_minute) 284 | end_time = datetime.time(end_hour, end_minute) 285 | 286 | return RandomModeStatusRequestedNotification(is_active=is_active, active_on_weekdays=active_on_weekdays, start_isotime=start_time.isoformat(timespec='minutes'), end_isotime=end_time.isoformat(timespec='minutes')) 287 | 288 | if payload[0:2] == b'\x15\x00': 289 | was_successful = False 290 | if payload[2:3] == b'\x00': 291 | was_successful = True 292 | 293 | return RandomModeChangedNotification(was_successful=was_successful) 294 | 295 | if payload[0:2] == b'\x04\x00': 296 | is_power_active = False 297 | if payload[2:3] == b'\x01': 298 | is_power_active = True 299 | 300 | power_in_milliwatt = int.from_bytes(payload[3:6], 'big') 301 | voltage_in_volt = int.from_bytes(payload[6:7], 'big') 302 | current_in_milliampere = int.from_bytes(payload[7:9], 'big') 303 | frequency_in_hertz = int.from_bytes(payload[9:10], 'big') 304 | total_consumption_in_kilowatt_hour = int.from_bytes(payload[12:16], 'big') 305 | 306 | return MeasurementRequestedNotification(is_power_active=is_power_active, power_in_milliwatt=power_in_milliwatt, voltage_in_volt=voltage_in_volt, current_in_milliampere=current_in_milliampere, frequency_in_hertz=frequency_in_hertz, total_consumption_in_kilowatt_hour=total_consumption_in_kilowatt_hour) 307 | 308 | if payload[0:2] == b'\x0c\x00': 309 | consumptions = [] 310 | for i in range((len(payload)-2) // 4): 311 | consumptions.insert(0, int.from_bytes(payload[2 + 4*i:2 + 4*i + 3], 'big')) 312 | 313 | # notification does not contain measurement for current month 314 | consumptions.insert(0, None) 315 | 316 | return ConsumptionOfLast12MonthsRequestedNotification(consumption_n_months_ago_in_watt_hour=consumptions) 317 | 318 | if payload[0:2] == b'\x0b\x00': 319 | consumptions = [] 320 | for i in range((len(payload)-2) // 4): 321 | consumptions.insert(0, int.from_bytes(payload[2 + 4*i:2 + 4*i + 3], 'big')) 322 | 323 | # notification does not contain measurement for today 324 | consumptions.insert(0, None) 325 | 326 | return ConsumptionOfLast30DaysRequestedNotification(consumption_n_days_ago_in_watt_hour=consumptions) 327 | 328 | if payload[0:2] == b'\x0a\x00': 329 | consumptions = [] 330 | for i in range((len(payload)-2) // 2): 331 | consumptions.insert(0, int.from_bytes(payload[2 + 2*i:2 + 2*(i+1)], 'big')) 332 | 333 | return ConsumptionOfLast23HoursRequestedNotification(consumption_n_hours_ago_in_watt_hour=consumptions) 334 | 335 | if payload[0:3] == b'\x0f\x00\x02': 336 | return ConsumptionResetNotification(was_successful=True) 337 | 338 | if payload[0:3] == b'\x0f\x00\x00': 339 | return FactoryResetNotification(was_successful=True) 340 | 341 | if payload[0:2] == b'\x02\x00': 342 | return DeviceNameChangedNotification(was_successful=True) 343 | 344 | if payload[0:2] == b'\x11\x00': 345 | serial = payload[2:-2].decode('utf-8') 346 | 347 | return DeviceSerialRequestedNotification(serial=serial) 348 | 349 | raise Exception('Unsupported message') 350 | -------------------------------------------------------------------------------- /sem6000/repeat_on_failure_decorator.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | def RepeatOnFailureDecorator(delays_in_seconds=None): 5 | if delays_in_seconds is None: 6 | delays_in_seconds = [0.1, 0.2, 0.4, 1.6, 3.2, 6.4] 7 | 8 | def Decorator(function): 9 | def decorated_function(*s, **d): 10 | def reconnect(): 11 | reconnectable = s[0] 12 | reconnectable._reconnect() 13 | 14 | def debug(msg): 15 | debuggable = s[0] 16 | 17 | if debuggable.debug: 18 | print(msg, file=sys.stderr) 19 | 20 | tries = 0 21 | for delay_in_seconds in delays_in_seconds: 22 | try: 23 | return function(*s, **d) 24 | except Exception as e: 25 | if tries == len(delays_in_seconds)-1: 26 | debug("command failed after " + str(tries) + " retries") 27 | 28 | raise e 29 | 30 | debug("command failed (" + str(tries) + " retries) - repeating after " + str(delay_in_seconds) + " seconds...") 31 | 32 | tries += 1 33 | time.sleep(delay_in_seconds) 34 | reconnect() 35 | 36 | return decorated_function 37 | 38 | return Decorator 39 | -------------------------------------------------------------------------------- /sem6000/sem6000.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import datetime 3 | import sys 4 | 5 | from .bluetooth_lowenergy_interface.bluepy_interface import * 6 | from . import encoder 7 | from .message import * 8 | from . import parser 9 | from .repeat_on_failure_decorator import RepeatOnFailureDecorator 10 | from . import util 11 | 12 | 13 | class SEM6000Delegate(): 14 | def __init__(self, debug=False, hardware_version=None): 15 | self.debug = False 16 | if debug: 17 | self.debug = True 18 | 19 | self.hardware_version = hardware_version 20 | self._raw_notifications = [] 21 | 22 | self._parser = parser.MessageParser(hardware_version=self.hardware_version) 23 | 24 | def __call__(self, characteristic_uuid, data): 25 | self._handle_notification(characteristic_uuid, data) 26 | 27 | def _handle_notification(self, characteristic_uuid, data): 28 | self._raw_notifications.append(data) 29 | 30 | def has_final_raw_notification(self): 31 | if len(self._raw_notifications) == 0: 32 | return False 33 | 34 | last_notification = self._raw_notifications[-1] 35 | 36 | if len(last_notification) < 2: 37 | return False 38 | 39 | # command b'\x04\x00' lacks suffix b'\xff\xff' 40 | if last_notification[2:4] == b'\x04\x00': 41 | return True 42 | 43 | # in hardware version >= 3 message b'\x10\x00' (SettingsRequestedNotification) 44 | # has two extra bytes behind b'\xff\xff' 45 | if self.hardware_version >= 3 and last_notification[2:4] == b'\x10\x00': 46 | return last_notification[-4:] == b'\xff\xff\x00\x00' 47 | 48 | return ( last_notification[-2:] == b'\xff\xff' ) 49 | 50 | def consume_notification(self): 51 | exception = None 52 | notification = None 53 | 54 | data = b'' 55 | for n in self._raw_notifications: 56 | data += n 57 | 58 | try: 59 | if not self.has_final_raw_notification(): 60 | raise Exception("Incomplete notification data") 61 | 62 | notification = self._parser.parse(data) 63 | except Exception as e: 64 | if self.debug: 65 | print("received data: " + str(binascii.hexlify(data)) + " (Unknown Notification)", file=sys.stderr) 66 | raise e 67 | 68 | if self.debug: 69 | print("received data: " + str(binascii.hexlify(data)) + " (" + str(notification) + ")", file=sys.stderr) 70 | 71 | 72 | while len(self._raw_notifications): 73 | self._raw_notifications.pop(0) 74 | 75 | return notification 76 | 77 | def reset_notification_data(self): 78 | self._raw_notifications.clear() 79 | 80 | 81 | class SEM6000(): 82 | SERVICECLASS_UUID='0000fff0-0000-1000-8000-00805f9b34fb' 83 | CHARACTERISTIC_UUID_NAME='00002a00-0000-1000-8000-00805f9b34fb' 84 | CHARACTERISTIC_UUID_CONTROL='0000fff3-0000-1000-8000-00805f9b34fb' 85 | CHARACTERISTIC_UUID_RESPONSE='0000fff4-0000-1000-8000-00805f9b34fb' 86 | CHARACTERISTIC_UUID_VERSION='0000fff1-0000-1000-8000-00805f9b34fb' 87 | 88 | def __init__(self, deviceAddr=None, pin=None, bluetooth_device='hci0', timeout=5, debug=False): 89 | """ Create a new SEM6000() instance 90 | 91 | Parameters: 92 | deviceAddr - Optional, MAC address of a remote device to connect to immediately, i.e. '00:11:22:33:44:55'. 93 | pin - Optional, 4 digit numeric pin, i.e. '0000'. 94 | bluetooth_device - Optional, bluetooth device name to use. Default: 'hci0' 95 | timeout - Optional, maximum time in seconds to wait for a response from the device. Default: 3 96 | debug - Optional, if set to true commands and responses are printed to sys.stderr 97 | """ 98 | self.timeout = timeout 99 | self.debug = debug 100 | self.hardware_version = None 101 | 102 | self.connection_settings = {} 103 | 104 | self.pin = None 105 | 106 | self._encoder = encoder.MessageEncoder() 107 | 108 | self._bluetooth_lowenergy_interface = BluePyBtLeInterface(bluetooth_device=bluetooth_device) 109 | 110 | if not deviceAddr is None: 111 | self.connect(deviceAddr) 112 | 113 | if not pin is None: 114 | self.authorize(pin) 115 | 116 | def _disconnect(self): 117 | if self._bluetooth_lowenergy_interface: 118 | self._bluetooth_lowenergy_interface.disconnect() 119 | 120 | return True 121 | 122 | return False 123 | 124 | def _reconnect(self): 125 | self._disconnect() 126 | 127 | if self.debug: 128 | print("connecting to " + str(self.connection_settings["device_address"]) + " ...", file=sys.stderr) 129 | 130 | try: 131 | self._bluetooth_lowenergy_interface.connect(self.connection_settings["device_address"]) 132 | 133 | # this is necessary since hardware version = 3 134 | self._bluetooth_lowenergy_interface.set_mtu(160); 135 | except Exception as e: 136 | self._disconnect() 137 | raise e 138 | 139 | #get hardware version 140 | device_info = self._bluetooth_lowenergy_interface.read_from_characteristic(self.CHARACTERISTIC_UUID_VERSION) 141 | self.hardware_version = int(device_info[13]) 142 | 143 | if self.debug: 144 | print("hardware_version: " + str(self.hardware_version), file=sys.stderr) 145 | 146 | # hardware version >= 3 does not support writing to 147 | # characteristics with confirmation-response 148 | if self.hardware_version < 3: 149 | self._bluetooth_lowenergy_interface.enable_notifications() 150 | 151 | self._delegate = SEM6000Delegate(self.debug, hardware_version = self.hardware_version) 152 | self._bluetooth_lowenergy_interface.add_notification_handler(self._delegate._handle_notification) 153 | 154 | if self.pin: 155 | try: 156 | self.authorize(self.pin) 157 | except Exception as e: 158 | self._disconnect() 159 | raise e 160 | 161 | def _is_connected(self): 162 | if self._bluetooth_lowenergy_interface is None: 163 | return False 164 | 165 | return self._bluetooth_lowenergy_interface.is_connected() 166 | 167 | def _send_command(self, command): 168 | encoded_command = self._encoder.encode(command) 169 | 170 | if self.debug: 171 | print("sent data: " + str(binascii.hexlify(encoded_command)) + " (" + str(command) + ")", file=sys.stderr) 172 | 173 | self._delegate.reset_notification_data() 174 | 175 | if not self._is_connected(): 176 | if self.connection_settings["device_address"] and self.pin: 177 | self._reconnect() 178 | else: 179 | raise Exception("Not connected and no deviceAddress / pin set") 180 | 181 | self._bluetooth_lowenergy_interface.write_to_characteristic(SEM6000.CHARACTERISTIC_UUID_CONTROL, encoded_command) 182 | self._wait_for_notifications() 183 | 184 | def _wait_for_notifications(self): 185 | while True: 186 | if not self._bluetooth_lowenergy_interface.wait_for_notifications(self.timeout): 187 | break 188 | 189 | if self._delegate.has_final_raw_notification(): 190 | break 191 | 192 | def _consume_notification(self): 193 | return self._delegate.consume_notification() 194 | 195 | @RepeatOnFailureDecorator() 196 | def connect(self, device_address): 197 | """ 198 | Connect to a remote device. 199 | 200 | Parameters: 201 | device_address - MAC address to connect to, i.e. '00:11:22:33:44:55'. 202 | """ 203 | self.connection_settings["device_address"] = device_address 204 | 205 | return self._reconnect() 206 | 207 | def disconnect(self): 208 | """ 209 | Disconnect from the current remote device. 210 | """ 211 | return self._disconnect() 212 | 213 | @RepeatOnFailureDecorator() 214 | def discover(timeout=5, bluetooth_device='hci0'): 215 | """ 216 | Discover remote devices. 217 | 218 | This method needs special permissions. 219 | 220 | Parameters: 221 | timeout - Optional, time in seconds to wait for devices to respond. Default: 5 222 | bluetooth_device - Optional, bluetooth device name to use. Default: 'hciß' 223 | """ 224 | bluetooth_lowenergy_interface = BluePyBtLeInterface(bluetooth_device=bluetooth_device) 225 | 226 | return bluetooth_lowenergy_interface.discover(timeout, service_uuids=[SEM6000.SERVICECLASS_UUID]) 227 | 228 | @RepeatOnFailureDecorator() 229 | def request_device_name(self): 230 | """ 231 | Request the name of the remote device. 232 | 233 | Returns a DeviceNameRequestedNotification. 234 | """ 235 | data = self._bluetooth_lowenergy_interface.read_from_characteristic(SEM6000.CHARACTERISTIC_UUID_NAME) 236 | 237 | if self.debug: 238 | print("received data: " + str(binascii.hexlify(data)), file=sys.stderr) 239 | 240 | device_name = data.decode(encoding='utf-8') 241 | 242 | return DeviceNameRequestedNotification(device_name) 243 | 244 | @RepeatOnFailureDecorator() 245 | def authorize(self, pin): 246 | """ 247 | Authorize on the connected device. 248 | 249 | Parameters: 250 | pin - 4 digit PIN, i.e. '0000' 251 | 252 | Returns an AuthorizedNotification. 253 | """ 254 | command = AuthorizeCommand(pin) 255 | self._send_command(command) 256 | notification = self._consume_notification() 257 | 258 | if not isinstance(notification, AuthorizedNotification): 259 | raise Exception("Authentication failed") 260 | 261 | if notification.was_successful: 262 | self.pin = pin 263 | else: 264 | self.pin = None 265 | raise Exception("Authentication failed") 266 | 267 | return notification 268 | 269 | @RepeatOnFailureDecorator() 270 | def change_pin(self, new_pin): 271 | """ 272 | Change the pin on the remote device. 273 | 274 | Parameters: 275 | new_pin - 4 digit PIN to change the current PIN to, i.e. '0000' 276 | 277 | Returns a PinChangedNotification. 278 | """ 279 | command = ChangePinCommand(self.pin, new_pin) 280 | self._send_command(command) 281 | notification = self._consume_notification() 282 | 283 | if self.hardware_version >= 3: 284 | # hardware version >= 3 does not reply with a PinChangedNotification 285 | if not isinstance(notification, AuthorizedNotification) or not notification.was_successful: 286 | raise Exception("Change PIN failed") 287 | else: 288 | if not isinstance(notification, PinChangedNotification) or not notification.was_successful: 289 | raise Exception("Change PIN failed") 290 | 291 | return notification 292 | 293 | @RepeatOnFailureDecorator() 294 | def reset_pin(self): 295 | """ 296 | Reset the pin to 0000 on the remote device. 297 | 298 | Returns a PinResetNotification. 299 | """ 300 | command = ResetPinCommand() 301 | self._send_command(command) 302 | notification = self._consume_notification() 303 | 304 | if not isinstance(notification, PinResetNotification) or not notification.was_successful: 305 | raise Exception("Reset PIN failed") 306 | 307 | return notification 308 | 309 | @RepeatOnFailureDecorator() 310 | def power_on(self): 311 | """ 312 | Tell the remote device to turn the power on. 313 | 314 | Returns a PowerSwitchedNotification. 315 | """ 316 | command = PowerSwitchCommand(True) 317 | self._send_command(command) 318 | notification = self._consume_notification() 319 | 320 | if not isinstance(notification, PowerSwitchedNotification) or not notification.was_successful: 321 | raise Exception("Power on failed") 322 | 323 | return notification 324 | 325 | @RepeatOnFailureDecorator() 326 | def power_off(self): 327 | """ 328 | Tell the remote device to turn the power off. 329 | 330 | Returns a PowerSwitchedNotification. 331 | """ 332 | command = PowerSwitchCommand(False) 333 | self._send_command(command) 334 | notification = self._consume_notification() 335 | 336 | if not isinstance(notification, PowerSwitchedNotification) or not notification.was_successful: 337 | raise Exception("Power off failed") 338 | 339 | return notification 340 | 341 | @RepeatOnFailureDecorator() 342 | def nightmode_on(self): 343 | """ 344 | Activate nightmode on the remote device. 345 | 346 | Returns a NightmodeChangedNotification. 347 | """ 348 | command = ChangeNightmodeCommand(True) 349 | self._send_command(command) 350 | notification = self._consume_notification() 351 | 352 | if self.hardware_version >= 3: 353 | # hardware version >= 3 for some reason replies with a FactoryResetNotification here 354 | if not isinstance(notification, FactoryResetNotification) or not notification.was_successful: 355 | raise Exception("Nightmode on failed") 356 | else: 357 | if not isinstance(notification, NightmodeChangedNotification) or not notification.was_successful: 358 | raise Exception("Nightmode on failed") 359 | 360 | return notification 361 | 362 | @RepeatOnFailureDecorator() 363 | def nightmode_off(self): 364 | """ 365 | Disable nightmode on the remote device. 366 | 367 | Returns a NightmodeChangedNotification. 368 | """ 369 | command = ChangeNightmodeCommand(False) 370 | self._send_command(command) 371 | notification = self._consume_notification() 372 | 373 | if self.hardware_version >= 3: 374 | # hardware version >= 3 for some reason replies with a FactoryResetNotification here 375 | if not isinstance(notification, FactoryResetNotification) or not notification.was_successful: 376 | raise Exception("Nightmode off failed") 377 | else: 378 | if not isinstance(notification, NightmodeChangedNotification) or not notification.was_successful: 379 | raise Exception("Nightmode off failed") 380 | 381 | return notification 382 | 383 | @RepeatOnFailureDecorator() 384 | def change_date_and_time(self, isodatetime): 385 | """ 386 | Set date and time on the remote device. 387 | 388 | Parameters: 389 | isodatetime - ISO string representing date and time, i.e. '2020-01-01T10:00' 390 | 391 | Returns a DateAndTimeChangedNotification. 392 | """ 393 | command = SynchronizeDateAndTimeCommand(isodatetime) 394 | self._send_command(command) 395 | notification = self._consume_notification() 396 | 397 | if not isinstance(notification, DateAndTimeChangedNotification) or not notification.was_successful: 398 | raise Exception("Set date and time failed") 399 | 400 | return notification 401 | 402 | @RepeatOnFailureDecorator() 403 | def request_settings(self): 404 | """ 405 | Request the current settings from the remote device. 406 | 407 | Returns a SettingsRequestedNotification. 408 | """ 409 | command = RequestSettingsCommand() 410 | self._send_command(command) 411 | notification = self._consume_notification() 412 | 413 | if not isinstance(notification, SettingsRequestedNotification): 414 | raise Exception("Request settings failed") 415 | 416 | return notification 417 | 418 | @RepeatOnFailureDecorator() 419 | def change_power_limit(self, power_limit_in_watt): 420 | """ 421 | Set the power limit when the remote device should be automatically turn off. 422 | 423 | Returns a PowerLimitChangedNotification. 424 | """ 425 | command = ChangePowerLimitCommand(power_limit_in_watt=int(power_limit_in_watt)) 426 | self._send_command(command) 427 | notification = self._consume_notification() 428 | 429 | if not isinstance(notification, PowerLimitChangedNotification) or not notification.was_successful: 430 | raise Exception("Set power limit failed") 431 | 432 | return notification 433 | 434 | @RepeatOnFailureDecorator() 435 | def change_prices(self, normal_price_in_cent, reduced_period_price_in_cent): 436 | """ 437 | Set the power prices. 438 | 439 | Parameters: 440 | normal_price_in_cent - Power price in cents. 441 | reduced_period_price_in_cent - Power price in cents during reduced period. 442 | 443 | Returns a PricesChangedNotification. 444 | """ 445 | command = ChangePricesCommand(normal_price_in_cent=int(normal_price_in_cent), reduced_period_price_in_cent=int(reduced_period_price_in_cent)) 446 | self._send_command(command) 447 | notification = self._consume_notification() 448 | 449 | if self.hardware_version >= 3: 450 | # hardware version >= 3 for some reason replies with a FactoryResetNotification here 451 | if not isinstance(notification, FactoryResetNotification) or not notification.was_successful: 452 | raise Exception("Set prices failed") 453 | else: 454 | if not isinstance(notification, PricesChangedNotification) or not notification.was_successful: 455 | raise Exception("Set prices failed") 456 | 457 | return notification 458 | 459 | @RepeatOnFailureDecorator() 460 | def change_reduced_period(self, is_active, start_isotime, end_isotime): 461 | """ 462 | Sets start and end time of the reduced period. 463 | 464 | Parameters: 465 | is_active - True if reduced prices should be used, False if not. 466 | start_isotime - ISO start time of the reduced period, i.e. '10:00' 467 | end_isotime - ISO end time of the reduced period, i.e. '20:00' 468 | 469 | Returns a ReducedPeriodChangedNotification. 470 | """ 471 | command = ChangeReducedPeriodCommand( 472 | is_active=util._parse_boolean(is_active), 473 | start_isotime=start_isotime, 474 | end_isotime=end_isotime) 475 | 476 | self._send_command(command) 477 | notification = self._consume_notification() 478 | 479 | if self.hardware_version >= 3: 480 | # hardware version >= 3 for some reason replies with a FactoryResetNotification here 481 | if not isinstance(notification, FactoryResetNotification) or not notification.was_successful: 482 | raise Exception("Set reduced period failed") 483 | else: 484 | if not isinstance(notification, ReducedPeriodChangedNotification) or not notification.was_successful: 485 | raise Exception("Set reduced period failed") 486 | 487 | return notification 488 | 489 | @RepeatOnFailureDecorator() 490 | def request_timer_status(self): 491 | """ 492 | Request the current status of the timer. 493 | 494 | Returns a TimerStatusRequestedNotification. 495 | """ 496 | command = RequestTimerStatusCommand() 497 | self._send_command(command) 498 | notification = self._consume_notification() 499 | 500 | if not isinstance(notification, TimerStatusRequestedNotification): 501 | raise Exception("Request timer status failed") 502 | 503 | return notification 504 | 505 | @RepeatOnFailureDecorator() 506 | def activate_timer(self, is_action_turn_on, delay_isotime): 507 | """ 508 | Activate the timer. 509 | 510 | Parameters: 511 | is_action_turn_on - True if the power should be turned on after the delay has passed, False if the power should be turned off. 512 | delay_isotime - Delay in iso time format, i.e. '00:00:05' for 5 seconds. 513 | 514 | Returns a TimerSetNotification. 515 | """ 516 | time = datetime.time.fromisoformat(delay_isotime) 517 | timedelta = datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) 518 | dt = datetime.datetime.now() + timedelta 519 | 520 | command = SetTimerCommand( 521 | is_reset_timer=False, 522 | is_action_turn_on=util._parse_boolean(is_action_turn_on), 523 | target_isodatetime=dt.isoformat(timespec='seconds')) 524 | 525 | self._send_command(command) 526 | notification = self._consume_notification() 527 | 528 | if not isinstance(notification, TimerSetNotification) or not notification.was_successful: 529 | raise Exception("Set timer failed") 530 | 531 | return notification 532 | 533 | @RepeatOnFailureDecorator() 534 | def activate_timer_at(self, is_action_turn_on, target_isodatetime): 535 | """ 536 | Activate the timer at the specified date and time. 537 | 538 | Parameters: 539 | is_action_turn_on - True if the power should be turned on, False if the power should be turned off. 540 | target_isodatetime - iso date and time format, i.e. '2020-01-01T00:00:05'. 541 | 542 | Returns a TimerSetNotification. 543 | """ 544 | command = SetTimerCommand( 545 | is_reset_timer=False, 546 | is_action_turn_on=util._parse_boolean(is_action_turn_on), 547 | target_isodatetime=target_isodatetime) 548 | 549 | self._send_command(command) 550 | notification = self._consume_notification() 551 | 552 | if not isinstance(notification, TimerSetNotification) or not notification.was_successful: 553 | raise Exception("Set timer failed") 554 | 555 | return notification 556 | 557 | @RepeatOnFailureDecorator() 558 | def reset_timer(self): 559 | """ 560 | Stop and reset the timer. 561 | 562 | Returns a TimerSetNotification. 563 | """ 564 | command = SetTimerCommand(is_reset_timer=True, is_action_turn_on=False) 565 | self._send_command(command) 566 | notification = self._consume_notification() 567 | 568 | if not isinstance(notification, TimerSetNotification) or not notification.was_successful: 569 | raise Exception("Reset timer failed") 570 | 571 | return notification 572 | 573 | @RepeatOnFailureDecorator() 574 | def request_scheduler(self): 575 | """ 576 | Request all currently set schedulers. 577 | 578 | Returns a SchedulerRequestedNotification. 579 | """ 580 | command = RequestSchedulerCommand(page_number=0) 581 | self._send_command(command) 582 | notification = self._consume_notification() 583 | 584 | if not isinstance(notification, SchedulerRequestedNotification): 585 | raise Exception('Request scheduler 1st page failed') 586 | 587 | max_page_number = notification.number_of_schedulers // 4 588 | for page_number in range(1, max_page_number+1): 589 | command = RequestSchedulerCommand(page_number=page_number) 590 | self._send_command(command) 591 | further_notification = self._consume_notification() 592 | 593 | if not isinstance(further_notification, SchedulerRequestedNotification): 594 | raise Exception('Request scheduler 2nd page failed') 595 | 596 | notification.scheduler_entries.extend(further_notification.scheduler_entries) 597 | 598 | return notification 599 | 600 | @RepeatOnFailureDecorator() 601 | def add_onetime_scheduler(self, is_active, is_action_turn_on, isodatetime): 602 | """ 603 | Add a scheduler entry occuring at a specific date and time. 604 | 605 | Parameters: 606 | is_active - True if the scheduler entry should be active, else False. 607 | is_action_turn_on - True if the power should be turned on, False if the power should be turned off. 608 | isodatetime - ISO date and time for when the scheduler entry should be executed, i.e. '2020-01-01T10:00' 609 | 610 | Returns a SchedulerChangedNotification. 611 | """ 612 | command = AddSchedulerCommand( 613 | OneTimeScheduler( 614 | is_active=util._parse_boolean(is_active), 615 | is_action_turn_on=util._parse_boolean(is_action_turn_on), 616 | isodatetime=isodatetime 617 | )) 618 | 619 | self._send_command(command) 620 | notification = self._consume_notification() 621 | 622 | if not isinstance(notification, SchedulerChangedNotification) or not notification.was_successful: 623 | raise Exception("Add scheduler failed") 624 | 625 | return notification 626 | 627 | @RepeatOnFailureDecorator() 628 | def edit_onetime_scheduler(self, slot_id, is_active, is_action_turn_on, isodatetime): 629 | """ 630 | Edit an existing scheduler entry occuring at a specific date and time. 631 | 632 | Parameters: 633 | slot_id - id of the slot where the scheduler entry is currently stored at. 634 | is_active - True if the scheduler entry should be active, else False. 635 | is_action_turn_on - True if the power should be turned on, False if the power should be turned off. 636 | isodatetime - ISO date and time for when the scheduler entry should be executed, i.e. '2020-01-01T10:00' 637 | 638 | Returns a SchedulerChangedNotification. 639 | """ 640 | command = EditSchedulerCommand( 641 | slot_id=int(slot_id), 642 | scheduler=OneTimeScheduler( 643 | is_active=util._parse_boolean(is_active), 644 | is_action_turn_on=util._parse_boolean(is_action_turn_on), 645 | isodatetime=isodatetime 646 | )) 647 | 648 | self._send_command(command) 649 | notification = self._consume_notification() 650 | 651 | if not isinstance(notification, SchedulerChangedNotification) or not notification.was_successful: 652 | raise Exception("Edit scheduler failed") 653 | 654 | return notification 655 | 656 | @RepeatOnFailureDecorator() 657 | def add_repeated_scheduler(self, is_active, is_action_turn_on, repeat_on_weekdays, isotime): 658 | """ 659 | Add a scheduler entry that will be repeated regulary. 660 | 661 | Parameters: 662 | is_active - True if the scheduler entry should be active, else False. 663 | is_action_turn_on - True if the power should be turned on, False if the power should be turned off. 664 | repeat_on_weekdays - Comma separated list of Weekdays the scheduler should be repeated on, i.e. 'Mon,Wed,Fri' 665 | isotime - ISO time for when the scheduler entry should be executed, i.e. '10:00' 666 | 667 | Returns a SchedulerChangedNotification. 668 | """ 669 | command = AddSchedulerCommand( 670 | RepeatedScheduler( 671 | is_active=util._parse_boolean(is_active), 672 | is_action_turn_on=util._parse_boolean(is_action_turn_on), 673 | repeat_on_weekdays=util._parse_weekdays_list(repeat_on_weekdays), 674 | isotime=isotime 675 | )) 676 | 677 | self._send_command(command) 678 | notification = self._consume_notification() 679 | 680 | if not isinstance(notification, SchedulerChangedNotification) or not notification.was_successful: 681 | raise Exception("Add scheduler failed") 682 | 683 | return notification 684 | 685 | @RepeatOnFailureDecorator() 686 | def edit_repeated_scheduler(self, slot_id, is_active, is_action_turn_on, repeat_on_weekdays, isotime): 687 | """ 688 | Edit an existing scheduler entry that will be repeated regulary. 689 | 690 | Parameters: 691 | slot_id - id of the slot where the scheduler entry is currently stored at. 692 | is_active - True if the scheduler entry should be active, else False. 693 | is_action_turn_on - True if the power should be turned on, False if the power should be turned off. 694 | repeat_on_weekdays - Comma separated list of Weekdays the scheduler should be repeated on, i.e. 'Mon,Wed,Fri' 695 | isotime - ISO time for when the scheduler entry should be executed, i.e. '10:00' 696 | 697 | Returns a SchedulerChangedNotification. 698 | """ 699 | command = EditSchedulerCommand( 700 | slot_id=int(slot_id), 701 | scheduler=RepeatedScheduler( 702 | is_active=util._parse_boolean(is_active), 703 | is_action_turn_on=util._parse_boolean(is_action_turn_on), 704 | repeat_on_weekdays=util._parse_weekdays_list(repeat_on_weekdays), 705 | isotime=isotime 706 | )) 707 | 708 | self._send_command(command) 709 | notification = self._consume_notification() 710 | 711 | if not isinstance(notification, SchedulerChangedNotification) or not notification.was_successful: 712 | raise Exception("Edit scheduler failed") 713 | 714 | return notification 715 | 716 | @RepeatOnFailureDecorator() 717 | def remove_scheduler(self, slot_id): 718 | """ 719 | Remove an existing scheduler entry. 720 | 721 | Parameters: 722 | slot_id - id of the slot where the scheduler entry is currently stored at. 723 | 724 | Returns a SchedulerChangedNotification. 725 | """ 726 | command = RemoveSchedulerCommand(slot_id=int(slot_id)) 727 | self._send_command(command) 728 | notification = self._consume_notification() 729 | 730 | if not isinstance(notification, SchedulerChangedNotification) or not notification.was_successful: 731 | raise Exception("Remove scheduler failed") 732 | 733 | return notification 734 | 735 | @RepeatOnFailureDecorator() 736 | def request_random_mode_status(self): 737 | """ 738 | Request the current status of the random mode from the remote device. 739 | 740 | Returns a RandomModeStatusRequestedNotification. 741 | """ 742 | command = RequestRandomModeStatusCommand() 743 | self._send_command(command) 744 | notification = self._consume_notification() 745 | 746 | if not isinstance(notification, RandomModeStatusRequestedNotification): 747 | raise Exception("Request random mode status failed") 748 | 749 | return notification 750 | 751 | @RepeatOnFailureDecorator() 752 | def change_random_mode(self, active_on_weekdays, start_isotime, end_isotime): 753 | """ 754 | Activate random mode on the remote device. 755 | 756 | Parameters: 757 | active_on_weekdays - Comma separated list of Weekdays the scheduler should be repeated on, i.e. 'Mon,Wed,Fri' 758 | start_isotime - ISO time of when random mode should start, i.e. '10:00' 759 | end_isotime - ISO time of when random mode should stop, i.e. '20:00' 760 | 761 | Returns a RandomModeChangedNotification. 762 | """ 763 | command = ChangeRandomModeCommand( 764 | is_active=True, 765 | active_on_weekdays=util._parse_weekdays_list(active_on_weekdays), 766 | start_isotime=start_isotime, 767 | end_isotime=end_isotime) 768 | 769 | self._send_command(command) 770 | notification = self._consume_notification() 771 | 772 | if not isinstance(notification, RandomModeChangedNotification) or not notification.was_successful: 773 | raise Exception("Set random mode failed") 774 | 775 | return notification 776 | 777 | @RepeatOnFailureDecorator() 778 | def reset_random_mode(self): 779 | """ 780 | Disable random mode on the remote device. 781 | 782 | Returns a RandomModeChangedNotification. 783 | """ 784 | command = ChangeRandomModeCommand( 785 | is_active=False, 786 | active_on_weekdays=[], 787 | start_isotime="00:00", 788 | end_isotime="00:00") 789 | 790 | self._send_command(command) 791 | notification = self._consume_notification() 792 | 793 | if not isinstance(notification, RandomModeChangedNotification) or not notification.was_successful: 794 | raise Exception("Set random mode failed") 795 | 796 | return notification 797 | 798 | @RepeatOnFailureDecorator() 799 | def request_measurement(self): 800 | """ 801 | Request current measurement values. 802 | 803 | Returns a MeasurementRequestedNotification. 804 | """ 805 | command = RequestMeasurementCommand() 806 | self._send_command(command) 807 | notification = self._consume_notification() 808 | 809 | if not isinstance(notification, MeasurementRequestedNotification): 810 | raise Exception("Request measurement failed") 811 | 812 | return notification 813 | 814 | @RepeatOnFailureDecorator() 815 | def request_consumption_of_last_12_months(self): 816 | """ 817 | Request consumption values of last 12 months. 818 | 819 | Date and time need to be set for the device to start collecting these data. 820 | 821 | Returns a ConsumptionOfLast12MonthsRequestedNotification. 822 | """ 823 | command = RequestConsumptionOfLast12MonthsCommand() 824 | self._send_command(command) 825 | notification = self._consume_notification() 826 | 827 | if not isinstance(notification, ConsumptionOfLast12MonthsRequestedNotification): 828 | raise("Request consumption of last 12 months failed") 829 | 830 | return notification 831 | 832 | @RepeatOnFailureDecorator() 833 | def request_consumption_of_last_30_days(self): 834 | """ 835 | Request consumption values of last 30 days. 836 | 837 | Date and time need to be set for the device to start collecting these data. 838 | 839 | Returns a ConsumptionOfLast30DaysRequestedNotification. 840 | """ 841 | command = RequestConsumptionOfLast30DaysCommand() 842 | self._send_command(command) 843 | notification = self._consume_notification() 844 | 845 | if not isinstance(notification, ConsumptionOfLast30DaysRequestedNotification): 846 | raise("Request consumption of last 30 days failed") 847 | 848 | return notification 849 | 850 | @RepeatOnFailureDecorator() 851 | def request_consumption_of_last_23_hours(self): 852 | """ 853 | Request consumption values of curent hour and last 23 hours. 854 | 855 | Date and time need to be set for the device to start collecting these data. 856 | 857 | Returns a ConsumptionOfLast23HoursRequestedNotification. 858 | """ 859 | command = RequestConsumptionOfLast23HoursCommand() 860 | self._send_command(command) 861 | notification = self._consume_notification() 862 | 863 | if not isinstance(notification, ConsumptionOfLast23HoursRequestedNotification): 864 | raise("Request consumption of last 23 hours failed") 865 | 866 | return notification 867 | 868 | @RepeatOnFailureDecorator() 869 | def reset_consumption(self): 870 | """ 871 | Reset consumption data. 872 | 873 | Returns a ResetConsumptionNoticiation. 874 | """ 875 | command = ResetConsumptionCommand() 876 | self._send_command(command) 877 | notification = self._consume_notification() 878 | 879 | if not isinstance(notification, ConsumptionResetNotification) or not notification.was_successful: 880 | raise("Reset consumption failed") 881 | 882 | return notification 883 | 884 | @RepeatOnFailureDecorator() 885 | def factory_reset(self): 886 | """ 887 | Reset the remote device to factory state. 888 | 889 | Returns a FactoryResetNotification. 890 | """ 891 | command = FactoryResetCommand() 892 | self._send_command(command) 893 | notification = self._consume_notification() 894 | 895 | if not isinstance(notification, FactoryResetNotification) or not notification.was_successful: 896 | raise("Factory reset failed") 897 | 898 | return notification 899 | 900 | @RepeatOnFailureDecorator() 901 | def change_device_name(self, new_name): 902 | """ 903 | Set the name of the remote device. 904 | 905 | Parameters: 906 | new_name - Name to be set. 907 | 908 | Returns a DeviceNameChangedNotification. 909 | """ 910 | command = ChangeDeviceNameCommand(new_name=new_name) 911 | self._send_command(command) 912 | notification = self._consume_notification() 913 | 914 | if not isinstance(notification, DeviceNameChangedNotification) or not notification.was_successful: 915 | raise Exception("Set device name failed") 916 | 917 | return notification 918 | 919 | @RepeatOnFailureDecorator() 920 | def request_device_serial(self): 921 | """ 922 | Request the serial number of the remote device. 923 | 924 | Returns a DeviceSerialRequestedNotification. 925 | """ 926 | if self.hardware_version >= 3: 927 | raise Exception("Devices with hardware version newer or equal 3 do not support to query the serial number") 928 | 929 | command = RequestDeviceSerialCommand() 930 | self._send_command(command) 931 | notification = self._consume_notification() 932 | 933 | if not isinstance(notification, DeviceSerialRequestedNotification): 934 | raise Exception("Request device serial failed") 935 | 936 | return notification 937 | -------------------------------------------------------------------------------- /sem6000/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moormaster/python3-voltcraft-sem6000/aa6835cf44ddc73badf6c4ec7ba121c891537e7a/sem6000/tests/__init__.py -------------------------------------------------------------------------------- /sem6000/tests/test_message_parser_and_encoder.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from sem6000.encoder import MessageEncoder 4 | from sem6000.parser import MessageParser 5 | from sem6000.message import * 6 | from sem6000 import util 7 | 8 | class MessagesTest(unittest.TestCase): 9 | def test_AuthorizedNotification(self): 10 | message = AuthorizedNotification(was_successful=True) 11 | encoded_message = MessageEncoder().encode(message) 12 | parsed_message = MessageParser().parse(encoded_message) 13 | 14 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 15 | 16 | def test_PinChangedNotification(self): 17 | message = PinChangedNotification(was_successful=True) 18 | encoded_message = MessageEncoder().encode(message) 19 | parsed_message = MessageParser().parse(encoded_message) 20 | 21 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 22 | 23 | def test_PinResetNotification(self): 24 | message = PinResetNotification(was_successful=True) 25 | encoded_message = MessageEncoder().encode(message) 26 | parsed_message = MessageParser().parse(encoded_message) 27 | 28 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 29 | 30 | def test_PowerSwitchedNotification(self): 31 | message = PowerSwitchedNotification(was_successful=True) 32 | encoded_message = MessageEncoder().encode(message) 33 | parsed_message = MessageParser().parse(encoded_message) 34 | 35 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 36 | 37 | def test_NightmodeChangedNotification(self): 38 | message = NightmodeChangedNotification(was_successful=True) 39 | encoded_message = MessageEncoder().encode(message) 40 | parsed_message = MessageParser().parse(encoded_message) 41 | 42 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 43 | 44 | def test_SynchroizeDateAndTimeNotification(self): 45 | message = DateAndTimeChangedNotification(was_successful=True) 46 | encoded_message = MessageEncoder().encode(message) 47 | parsed_message = MessageParser().parse(encoded_message) 48 | 49 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 50 | 51 | def test_SettingsRequestedNotification(self): 52 | message = SettingsRequestedNotification(is_reduced_period=True, normal_price_in_cent=100, reduced_period_price_in_cent=50, reduced_period_start_isotime="22:00", reduced_period_end_isotime="05:00", is_nightmode_active=True, power_limit_in_watt=500) 53 | encoded_message = MessageEncoder().encode(message) 54 | parsed_message = MessageParser().parse(encoded_message) 55 | 56 | self.assertEqual(True, parsed_message.is_reduced_period, 'reduced_period_is_active value differs') 57 | self.assertEqual(100, parsed_message.normal_price_in_cent, 'normal_price_in_cent value differs') 58 | self.assertEqual(50, parsed_message.reduced_period_price_in_cent, 'reduced_price value_in_cent differs') 59 | self.assertEqual("22:00", parsed_message.reduced_period_start_isotime, 'reduced_period_start_isotime value differs') 60 | self.assertEqual("05:00", parsed_message.reduced_period_end_isotime, 'reduced_period_end_isotime value differs') 61 | self.assertEqual(True, parsed_message.is_nightmode_active, 'is_nightmode_active value differs') 62 | self.assertEqual(500, parsed_message.power_limit_in_watt, 'power_limit_in_watt value differs') 63 | 64 | def test_PowerLimitChangedNotification(self): 65 | message = PowerLimitChangedNotification(was_successful=True) 66 | encoded_message = MessageEncoder().encode(message) 67 | parsed_message = MessageParser().parse(encoded_message) 68 | 69 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 70 | 71 | def test_PricesChangedNotification(self): 72 | message = PricesChangedNotification(was_successful=True) 73 | encoded_message = MessageEncoder().encode(message) 74 | parsed_message = MessageParser().parse(encoded_message) 75 | 76 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 77 | 78 | def test_ReducedPeriodChangedNotification(self): 79 | message = ReducedPeriodChangedNotification(was_successful=True) 80 | encoded_message = MessageEncoder().encode(message) 81 | parsed_message = MessageParser().parse(encoded_message) 82 | 83 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 84 | 85 | def test_TimerStatusRequestedNotification(self): 86 | message = TimerStatusRequestedNotification(is_active=True, is_action_turn_on=True, target_isodatetime="2004-03-12T12:34:12", original_timer_length_in_seconds=42) 87 | encoded_message = MessageEncoder().encode(message) 88 | parsed_message = MessageParser(year_diff=2000).parse(encoded_message) 89 | 90 | self.assertEqual(True, parsed_message.is_active, 'is_active value differs') 91 | self.assertEqual(True, parsed_message.is_action_turn_on, 'is_action_turn_on value differs') 92 | self.assertEqual("2004-03-12T12:34:12", parsed_message.target_isodatetime, 'target_isodatetime value differs') 93 | self.assertEqual(42, parsed_message.original_timer_length_in_seconds, 'original_timer_length_in_seconds value differs') 94 | 95 | def test_TimerSetNotification(self): 96 | message = TimerSetNotification(was_successful=True) 97 | 98 | encoded_message = MessageEncoder().encode(message) 99 | parsed_message = MessageParser().parse(encoded_message) 100 | 101 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 102 | 103 | def test_SchedulerRequestedNotification(self): 104 | scheduler_entries=[] 105 | for i in range(12): 106 | if i <= 6: 107 | repeat_on_weekdays = [] 108 | repeat_on_weekdays.append(util.Weekday.SUNDAY) 109 | repeat_on_weekdays.append(util.Weekday.MONDAY) 110 | repeat_on_weekdays.append(util.Weekday.TUESDAY) 111 | repeat_on_weekdays.append(util.Weekday.WEDNESDAY) 112 | repeat_on_weekdays.append(util.Weekday.THURSDAY) 113 | repeat_on_weekdays.append(util.Weekday.FRIDAY) 114 | repeat_on_weekdays.append(util.Weekday.SATURDAY) 115 | 116 | scheduler = RepeatedScheduler(is_active=True, is_action_turn_on=True, repeat_on_weekdays=repeat_on_weekdays, isotime="12:34") 117 | else: 118 | scheduler = OneTimeScheduler(is_active=True, is_action_turn_on=True, isodatetime="2020-12-03T12:34") 119 | 120 | scheduler_entries.append(SchedulerEntry(slot_id=i, scheduler=scheduler)) 121 | 122 | message = SchedulerRequestedNotification(number_of_schedulers=12, scheduler_entries=scheduler_entries) 123 | 124 | encoded_message = MessageEncoder().encode(message) 125 | parsed_message = MessageParser(year_diff=2000).parse(encoded_message) 126 | 127 | self.assertEqual(12, parsed_message.number_of_schedulers) 128 | self.assertEqual(12, len(parsed_message.scheduler_entries)) 129 | for i in range(12): 130 | repeat_on_weekday_expected = [] 131 | 132 | if i <= 6: 133 | repeat_on_weekday_expected.append(util.Weekday.SUNDAY) 134 | repeat_on_weekday_expected.append(util.Weekday.MONDAY) 135 | repeat_on_weekday_expected.append(util.Weekday.TUESDAY) 136 | repeat_on_weekday_expected.append(util.Weekday.WEDNESDAY) 137 | repeat_on_weekday_expected.append(util.Weekday.THURSDAY) 138 | repeat_on_weekday_expected.append(util.Weekday.FRIDAY) 139 | repeat_on_weekday_expected.append(util.Weekday.SATURDAY) 140 | 141 | self.assertEqual(i, parsed_message.scheduler_entries[i].slot_id, 'slot_id value differs on scheduler ' + str(i)) 142 | self.assertEqual(True, parsed_message.scheduler_entries[i].scheduler.is_active, 'is_active value differs on scheduler ' + str(i)) 143 | self.assertEqual(True, parsed_message.scheduler_entries[i].scheduler.is_action_turn_on, 'is_action_turn_on value differs on scheduler ' + str(i)) 144 | self.assertEqual(repeat_on_weekday_expected, parsed_message.scheduler_entries[i].scheduler.repeat_on_weekdays, 'repeat_on_weekdays value differs on scheduler ' + str(i)) 145 | 146 | if i <= 6: 147 | isotime = datetime.datetime.fromisoformat(parsed_message.scheduler_entries[i].scheduler.isodatetime).time().isoformat(timespec='minutes') 148 | self.assertEqual("12:34", isotime, 'isotime value differs on scheduler ' + str(i)) 149 | else: 150 | self.assertEqual("2020-12-03T12:34", parsed_message.scheduler_entries[i].scheduler.isodatetime, 'isodatetime value differs on scheduler ' + str(i)) 151 | 152 | def test_SchedulerChangedNotification(self): 153 | message = SchedulerChangedNotification(was_successful=True) 154 | encoded_message = MessageEncoder().encode(message) 155 | parsed_message = MessageParser().parse(encoded_message) 156 | 157 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 158 | 159 | def test_RandomModeStatusRequestedNotification(self): 160 | active_on_weekdays=[] 161 | active_on_weekdays.append(util.Weekday.SUNDAY) 162 | active_on_weekdays.append(util.Weekday.MONDAY) 163 | active_on_weekdays.append(util.Weekday.TUESDAY) 164 | active_on_weekdays.append(util.Weekday.WEDNESDAY) 165 | active_on_weekdays.append(util.Weekday.THURSDAY) 166 | active_on_weekdays.append(util.Weekday.FRIDAY) 167 | active_on_weekdays.append(util.Weekday.SATURDAY) 168 | 169 | message = RandomModeStatusRequestedNotification(is_active=True, active_on_weekdays=active_on_weekdays, start_isotime="10:30", end_isotime="18:45") 170 | encoded_message = MessageEncoder().encode(message) 171 | parsed_message = MessageParser().parse(encoded_message) 172 | 173 | active_on_weekdays_expected=[] 174 | active_on_weekdays_expected.append(util.Weekday.SUNDAY) 175 | active_on_weekdays_expected.append(util.Weekday.MONDAY) 176 | active_on_weekdays_expected.append(util.Weekday.TUESDAY) 177 | active_on_weekdays_expected.append(util.Weekday.WEDNESDAY) 178 | active_on_weekdays_expected.append(util.Weekday.THURSDAY) 179 | active_on_weekdays_expected.append(util.Weekday.FRIDAY) 180 | active_on_weekdays_expected.append(util.Weekday.SATURDAY) 181 | 182 | self.assertEqual(True, parsed_message.is_active, 'is_active value differs') 183 | self.assertEqual(active_on_weekdays_expected, parsed_message.active_on_weekdays, 'active_on_weekdays value differs') 184 | self.assertEqual("10:30", parsed_message.start_isotime, 'start_isotime value differs') 185 | self.assertEqual("18:45", parsed_message.end_isotime, 'end_isotime value differs') 186 | 187 | def test_RandomModeChangedNotification(self): 188 | message = RandomModeChangedNotification(was_successful=True) 189 | encoded_message = MessageEncoder().encode(message) 190 | parsed_message = MessageParser().parse(encoded_message) 191 | 192 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 193 | 194 | def test_MeasurementRequestedNotification(self): 195 | message = MeasurementRequestedNotification(is_power_active=True, power_in_milliwatt=80, voltage_in_volt=230, current_in_milliampere=1000, frequency_in_hertz=50, total_consumption_in_kilowatt_hour=1000) 196 | encoded_message = MessageEncoder().encode(message) 197 | parsed_message = MessageParser().parse(encoded_message) 198 | 199 | self.assertEqual(True, parsed_message.is_power_active, 'is_power_active value differs') 200 | self.assertEqual(80, parsed_message.power_in_milliwatt, 'power_in_milliwatt value differs') 201 | self.assertEqual(230, parsed_message.voltage_in_volt, 'voltage_in_volt value differs') 202 | self.assertEqual(1000, parsed_message.current_in_milliampere, 'current_in_milliampere value differs') 203 | self.assertEqual(50, parsed_message.frequency_in_hertz, 'frequency_in_hertz value differs') 204 | self.assertEqual(1000, parsed_message.total_consumption_in_kilowatt_hour, 'total_consumption_in_kilowatt_hour value differs') 205 | 206 | def test_ConsumptionOfLast12MonthsRequestedNotification(self): 207 | message = ConsumptionOfLast12MonthsRequestedNotification(consumption_n_months_ago_in_watt_hour=[None, 10,20,30,40,50,60,70,80,90,100,110,120]) 208 | encoded_message = MessageEncoder().encode(message) 209 | parsed_message = MessageParser().parse(encoded_message) 210 | 211 | self.assertEqual(13, len(parsed_message.consumption_n_months_ago_in_watt_hour), 'incorrect number of values') 212 | self.assertEqual([None, 10,20,30,40,50,60,70,80,90,100,110,120], parsed_message.consumption_n_months_ago_in_watt_hour, 'values for consumption in watt hour differ') 213 | 214 | def test_ConsumptionOfLast30DaysRequestedNotification(self): 215 | message = ConsumptionOfLast30DaysRequestedNotification(consumption_n_days_ago_in_watt_hour=[None, 10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170,180,190,200,210,220,230,240,250,260,270,280,290,300]) 216 | encoded_message = MessageEncoder().encode(message) 217 | parsed_message = MessageParser().parse(encoded_message) 218 | 219 | self.assertEqual(31, len(parsed_message.consumption_n_days_ago_in_watt_hour), 'number of values differ') 220 | self.assertEqual([None, 10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170,180,190,200,210,220,230,240,250,260,270,280,290,300], parsed_message.consumption_n_days_ago_in_watt_hour, 'values for consumption in watt hour differ') 221 | 222 | def test_ConsumptionOfLast23HoursRequestedNotification(self): 223 | message = ConsumptionOfLast23HoursRequestedNotification(consumption_n_hours_ago_in_watt_hour=[10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240]) 224 | encoded_message = MessageEncoder().encode(message) 225 | parsed_message = MessageParser().parse(encoded_message) 226 | 227 | self.assertEqual(24, len(parsed_message.consumption_n_hours_ago_in_watt_hour), 'number of values differ') 228 | self.assertEqual([10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240], parsed_message.consumption_n_hours_ago_in_watt_hour, 'values for consumption in watt hour differ') 229 | 230 | def test_ConsumptionResetNotification(self): 231 | message = ConsumptionResetNotification(was_successful=True) 232 | encoded_message = MessageEncoder().encode(message) 233 | parsed_message = MessageParser().parse(encoded_message) 234 | 235 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 236 | 237 | def test_FactoryResetNotification(self): 238 | message = FactoryResetNotification(was_successful=True) 239 | encoded_message = MessageEncoder().encode(message) 240 | parsed_message = MessageParser().parse(encoded_message) 241 | 242 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 243 | 244 | def test_DeviceNameChangedNotification(self): 245 | message = DeviceNameChangedNotification(was_successful=True) 246 | encoded_message = MessageEncoder().encode(message) 247 | parsed_message = MessageParser().parse(encoded_message) 248 | 249 | self.assertEqual(True, parsed_message.was_successful, 'was_successful value differs') 250 | 251 | def test_DeviceSerialRequestedNotification(self): 252 | message = DeviceSerialRequestedNotification("ML01D10012000000") 253 | encoded_message = MessageEncoder().encode(message) 254 | parsed_message = MessageParser().parse(encoded_message) 255 | 256 | self.assertEqual("ML01D10012000000", message.serial, 'serial value differs') 257 | 258 | 259 | -------------------------------------------------------------------------------- /sem6000/util.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import enum 3 | 4 | class Weekday(enum.Enum): 5 | SUNDAY = 0 6 | MONDAY = 1 7 | TUESDAY = 2 8 | WEDNESDAY = 3 9 | THURSDAY = 4 10 | FRIDAY = 5 11 | SATURDAY = 6 12 | 13 | 14 | def _format_year_and_month(year, month): 15 | return "{:04}-{:02}".format(year, month) 16 | 17 | def _list_values_to_enum(enum_class, list_of_values): 18 | list_of_enums = [] 19 | for v in list_of_values: 20 | list_of_enums.append(enum_class(v)) 21 | return list_of_enums 22 | 23 | def _format_list_of_objects(format_object_lambda, list_of_objects): 24 | is_first = True 25 | formatted_string = "[" 26 | for o in list_of_objects: 27 | if not is_first: 28 | formatted_string += ", " 29 | formatted_string += format_object_lambda(o) 30 | is_first = False 31 | formatted_string += "]" 32 | 33 | return formatted_string 34 | 35 | 36 | def _parse_boolean(boolean_string): 37 | boolean_value = False 38 | 39 | if str(boolean_string).lower() == "true": 40 | boolean_value = True 41 | if str(boolean_string).lower() == "on": 42 | boolean_value = True 43 | if str(boolean_string).lower() == "1": 44 | boolean_value = True 45 | 46 | return boolean_value 47 | 48 | def _parse_list(list_input): 49 | if type(list_input) == list: 50 | list_value = list_input 51 | if type(list_input) == str: 52 | list_value = list_input.split(",") 53 | 54 | for i in range(len(list_value)): 55 | if type(list_value[i]) == str: 56 | list_value[i] = list_value[i].strip() 57 | 58 | return list_value 59 | 60 | def _parse_time_from_minutes(minutes): 61 | hour = minutes // 60 62 | minute = minutes - hour*60 63 | 64 | return datetime.time(hour, minute) 65 | 66 | def _parse_weekday(weekday): 67 | if isinstance(weekday, Weekday): 68 | return weekday 69 | 70 | if type(weekday) == int: 71 | return Weekday(weekday) 72 | 73 | weekday = weekday.lower() 74 | 75 | weekday_num = 0 76 | for weekday_str in ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]: 77 | if weekday_str in weekday or weekday == str(weekday_num): 78 | return Weekday(weekday_num) 79 | weekday_num += 1 80 | 81 | return None 82 | 83 | def _parse_weekdays_list(weekdays_list): 84 | weekdays=[] 85 | 86 | weekday_values_list = _parse_list(weekdays_list) 87 | for weekday_value in weekday_values_list: 88 | weekday = _parse_weekday(weekday_value) 89 | if weekday is None: 90 | continue 91 | weekdays.append(weekday) 92 | 93 | return weekdays 94 | 95 | -------------------------------------------------------------------------------- /settings_for_integration_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "device-name": "integrationTest name", 3 | "settings": { 4 | "reduced-period": { 5 | "is-active": true, 6 | "price-in-cent": 23, 7 | "start-isotime": "22:00", 8 | "end-isotime": "06:00" 9 | }, 10 | "normal-price-in-cent": 42, 11 | "is-nightmode-active": false, 12 | "power-limit-in-watt": 2000 13 | }, 14 | "random-mode": { 15 | "is-active": true, 16 | "active-on-weekdays": [ 17 | 0, 18 | 1, 19 | 2, 20 | 3, 21 | 4, 22 | 5, 23 | 6 24 | ], 25 | "start-isotime": "08:00", 26 | "end-isotime": "16:00" 27 | }, 28 | "timer": { 29 | "is-active": true, 30 | "is-action-turn-on": false, 31 | "isodatetime": "2040-01-01T12:34:56" 32 | }, 33 | "scheduler": { 34 | "number-of-schedulers": 4, 35 | "entries": { 36 | "9": { 37 | "is-active": true, 38 | "is-action-turn-on": true, 39 | "repeat-on-weekdays": [ 40 | 0, 41 | 1, 42 | 2, 43 | 3, 44 | 4, 45 | 5, 46 | 6 47 | ], 48 | "isotime": "08:00" 49 | }, 50 | "10": { 51 | "is-active": true, 52 | "is-action-turn-on": false, 53 | "repeat-on-weekdays": [ 54 | 0, 55 | 1, 56 | 2, 57 | 3, 58 | 4, 59 | 5, 60 | 6 61 | ], 62 | "isotime": "10:00" 63 | }, 64 | "11": { 65 | "is-active": true, 66 | "is-action-turn-on": true, 67 | "isodatetime": "2040-01-01T15:00" 68 | }, 69 | "12": { 70 | "is-active": true, 71 | "is-action-turn-on": false, 72 | "isodatetime": "2040-01-01T16:00" 73 | } 74 | } 75 | } 76 | } 77 | --------------------------------------------------------------------------------