python-snap7 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
snap7/util/db.py ADDED
@@ -0,0 +1,789 @@
1
+ """
2
+ This module contains utility functions for working with PLC DB objects.
3
+ There are functions to work with the raw bytearray data snap7 functions return
4
+ In order to work with this data you need to make python able to work with the
5
+ PLC bytearray data.
6
+
7
+ For example code see test_util.py and example.py in the example folder.
8
+
9
+
10
+ example::
11
+
12
+ spec/DB layout
13
+
14
+ # Byte index Variable name Datatype
15
+ layout=\"\"\"
16
+ 4 ID INT
17
+ 6 NAME STRING[6]
18
+
19
+ 12.0 test_bool1 BOOL
20
+ 12.1 test_bool2 BOOL
21
+ 12.2 test_bool3 BOOL
22
+ 12.3 test_bool4 BOOL
23
+ 12.4 test_bool5 BOOL
24
+ 12.5 test_bool6 BOOL
25
+ 12.6 test_bool7 BOOL
26
+ 12.7 test_bool8 BOOL
27
+ 13 testReal REAL
28
+ 17 testDword DWORD
29
+ \"\"\"
30
+
31
+ client = snap7.client.Client()
32
+ client.connect('192.168.200.24', 0, 3)
33
+
34
+ # this looks confusing but this means uploading from the PLC to YOU
35
+ # so downloading in the PC world :)
36
+
37
+ all_data = client.upload(db_number)
38
+
39
+ simple:
40
+
41
+ from snap7 import DB
42
+ db1 = DB(
43
+ db_number, # the db we use
44
+ all_data, # bytearray from the plc
45
+ layout, # layout specification DB variable data
46
+ # A DB specification is the specification of a
47
+ # DB object in the PLC you can find it using
48
+ # the dataview option on a DB object in PCS7
49
+
50
+ 17+2, # size of the specification 17 is start
51
+ # of last value
52
+ # which is a DWORD which is 2 bytes,
53
+
54
+ 1, # number of row's / specifications
55
+
56
+ id_field='ID', # field we can use to identify a row.
57
+ # default index is used
58
+ layout_offset=4, # sometimes specification does not start a 0
59
+ # like in our example
60
+ db_offset=0 # At which point in 'all_data' should we start
61
+ # reading. This could be that the specification
62
+ # does not start at 0
63
+ )
64
+
65
+ Now we can use db1 in python as a dict. if 'ID' contains
66
+ the 'test' we can identify the 'test' row in the all_data bytearray
67
+
68
+ To test of you layout matches the data from the plc you can
69
+ just print db1[0] or db['test'] in the example
70
+
71
+ db1['test']['test_bool1'] = 0
72
+
73
+ If we do not specify an id_field this should work to read out the
74
+ same data.
75
+
76
+ db1[0]['test_bool1']
77
+
78
+ to read and write a single Row from the plc. takes like 5ms!
79
+
80
+ db1['test'].write()
81
+
82
+ db1['test'].read(client)
83
+
84
+
85
+ """
86
+
87
+ import re
88
+ from logging import getLogger
89
+ from datetime import datetime, date, timedelta
90
+ from typing import Any, Optional, Union, Iterator, Tuple, Dict, Callable
91
+
92
+ from snap7 import Client
93
+ from snap7.type import Area, ValueType
94
+
95
+ from snap7.util import (
96
+ set_bool,
97
+ set_fstring,
98
+ set_string,
99
+ set_wstring,
100
+ set_real,
101
+ set_dword,
102
+ set_udint,
103
+ set_dint,
104
+ set_uint,
105
+ set_int,
106
+ set_word,
107
+ set_byte,
108
+ set_char,
109
+ set_wchar,
110
+ set_usint,
111
+ set_sint,
112
+ set_time,
113
+ set_lreal,
114
+ set_date,
115
+ set_tod,
116
+ set_dtl,
117
+ set_dt,
118
+ get_bool,
119
+ get_fstring,
120
+ get_string,
121
+ get_wstring,
122
+ get_real,
123
+ get_dword,
124
+ get_udint,
125
+ get_dint,
126
+ get_uint,
127
+ get_int,
128
+ get_word,
129
+ get_byte,
130
+ get_s5time,
131
+ get_dt,
132
+ get_usint,
133
+ get_sint,
134
+ get_time,
135
+ get_date,
136
+ get_tod,
137
+ get_lreal,
138
+ get_char,
139
+ get_wchar,
140
+ get_dtl,
141
+ )
142
+
143
+ logger = getLogger(__name__)
144
+
145
+
146
+ def parse_specification(db_specification: str) -> Dict[str, Any]:
147
+ """Create a db specification derived from a
148
+ dataview of a db in which the byte layout
149
+ is specified
150
+
151
+ Args:
152
+ db_specification: string formatted table with the indexes, aliases and types.
153
+
154
+ Returns:
155
+ Parsed DB specification.
156
+ """
157
+ parsed_db_specification = {}
158
+ pattern = r"""
159
+ (?P<index>\d+(\.\d+)?)\s+ # Match integer or decimal index
160
+ (?P<var_name>.*?)\s+ # Non-greedy match for variable name
161
+ (?P<_type>\S+)$ # Match type at end of line
162
+ """
163
+ regex = re.compile(pattern, re.VERBOSE)
164
+
165
+ for line in db_specification.split("\n"):
166
+ if line and not line.lstrip().startswith("#"):
167
+ match = regex.match(line.strip())
168
+ if match:
169
+ index = match.group("index")
170
+ var_name = match.group("var_name")
171
+ _type = match.group("_type")
172
+ var_name = var_name.strip()
173
+
174
+ parsed_db_specification[var_name] = (index, _type)
175
+
176
+ return parsed_db_specification
177
+
178
+
179
+ def print_row(data: bytearray) -> None:
180
+ """print a single db row in chr and str"""
181
+ index_line = ""
182
+ pri_line1 = ""
183
+ chr_line2 = ""
184
+ matcher = re.compile("[a-zA-Z0-9 ]")
185
+
186
+ for i, xi in enumerate(data):
187
+ # index
188
+ if not i % 5:
189
+ diff = len(pri_line1) - len(index_line)
190
+ index_line += diff * " "
191
+ index_line += str(i)
192
+ # i = i + (ws - len(i)) * ' ' + ','
193
+
194
+ # byte array line
195
+ str_v = str(xi)
196
+ pri_line1 += str(xi) + ","
197
+ # char line
198
+ c = chr(xi)
199
+ c = c if matcher.match(c) else " "
200
+ # align white space
201
+ w = len(str_v)
202
+ c = c + (w - 1) * " " + ","
203
+ chr_line2 += c
204
+
205
+ logger.info(index_line)
206
+ logger.info(pri_line1)
207
+ logger.info(chr_line2)
208
+
209
+
210
+ class DB:
211
+ """
212
+ Manage a DB bytearray block given a specification
213
+ of the Layout.
214
+
215
+ It is possible to have many repetitive instances of
216
+ a specification this is called a "row".
217
+
218
+ Probably most usecases there is just one row
219
+
220
+ Note:
221
+ This class has some of the semantics of a dict. In particular, the membership operators
222
+ (``in``, ``not it``), the access operator (``[]``), as well as the :func:`~DB.keys()` and
223
+ :func:`~DB.items()` methods work as usual. Iteration, on the other hand, happens on items
224
+ instead of keys (much like :func:`~DB.items()` method).
225
+
226
+ Attributes:
227
+ bytearray_: buffer data from the PLC.
228
+ specification: layout of the DB Rows.
229
+ row_size: bytes size of a db row.
230
+ layout_offset: at which byte in the row specificaion we
231
+ start reading the data.
232
+ db_offset: at which byte in the db starts reading.
233
+
234
+ Examples:
235
+ >>> db = DB()
236
+ >>> db[0]['test_bool1'] = "test"
237
+ >>> db.write(Client()) # puts data in plc
238
+ """
239
+
240
+ bytearray_: Optional[bytearray] = None # data from plc
241
+ specification: str # layout of db rows
242
+ id_field: Optional[str] = None # ID field of the rows
243
+ row_size: int = 0 # bytes size of a db row
244
+ layout_offset: int = 0 # at which byte in row specification should
245
+ db_offset: int = 0 # at which byte in db should we start reading?
246
+
247
+ # first fields could be status data.
248
+ # and only the last part could be control data
249
+ # now you can be sure you will never overwrite
250
+ # critical parts of db
251
+
252
+ def __init__(
253
+ self,
254
+ db_number: int,
255
+ bytearray_: bytearray,
256
+ specification: str,
257
+ row_size: int,
258
+ size: int,
259
+ id_field: Optional[str] = None,
260
+ db_offset: int = 0,
261
+ layout_offset: int = 0,
262
+ row_offset: int = 0,
263
+ area: Area = Area.DB,
264
+ ):
265
+ """Creates a new instance of the `Row` class.
266
+
267
+ Args:
268
+ db_number: number of the DB to read from. This value should be 0 if area!=Areas.DB.
269
+ bytearray_: initial buffer read from the PLC.
270
+ specification: layout of the PLC memory.
271
+ row_size: bytes size of a db row.
272
+ size: length of the memory area.
273
+ id_field: name to reference the row. Optional.
274
+ db_offset: at which byte in the db starts reading.
275
+ layout_offset: at which byte in the row specification we
276
+ start reading the data.
277
+ row_offset: offset between rows.
278
+ area: which memory area this row is representing.
279
+ """
280
+ self.db_number = db_number
281
+ self.size = size
282
+ self.row_size = row_size
283
+ self.id_field = id_field
284
+ self.area = area
285
+
286
+ self.db_offset = db_offset
287
+ self.layout_offset = layout_offset
288
+ self.row_offset = row_offset
289
+
290
+ self._bytearray = bytearray_
291
+ self.specification = specification
292
+ # loop over bytearray. make rowObjects
293
+ # store index of id_field to row objects
294
+ self.index: Dict[str, Row] = {}
295
+ self.make_rows()
296
+
297
+ def make_rows(self) -> None:
298
+ """Make each row for the DB."""
299
+ id_field = self.id_field
300
+ row_size = self.row_size
301
+ specification = self.specification
302
+ layout_offset = self.layout_offset
303
+ row_offset = self.row_offset
304
+
305
+ for i in range(self.size):
306
+ # calculate where row in bytearray starts
307
+ db_offset = i * (row_size + row_offset) + self.db_offset
308
+ # create a row object
309
+ row = Row(
310
+ self,
311
+ specification,
312
+ row_size=row_size,
313
+ db_offset=db_offset,
314
+ layout_offset=layout_offset,
315
+ row_offset=self.row_offset,
316
+ area=self.area,
317
+ )
318
+
319
+ # store row object
320
+ key = row[id_field] if id_field else i
321
+ if key and key in self.index:
322
+ msg = f"{key} not unique!"
323
+ logger.error(msg)
324
+ self.index[str(key)] = row
325
+
326
+ def __getitem__(self, key: str, default: Optional[None] = None) -> Union[None, "Row"]:
327
+ """Access a row of the table through its index.
328
+
329
+ Rows (values) are of type :class:`DB_Row`.
330
+
331
+ Notes:
332
+ This method has the same semantics as :class:`dict` access.
333
+ """
334
+ return self.index.get(key, default)
335
+
336
+ def __iter__(self) -> Iterator[Tuple[str, Any]]:
337
+ """Iterate over the items contained in the table, in the physical order they are contained
338
+ in memory.
339
+
340
+ Notes:
341
+ This method does not have the same semantics as :class:`dict` iteration. Instead, it
342
+ has the same semantics as the :func:`~DB.items` method, yielding ``(index, row)``
343
+ tuples.
344
+ """
345
+ yield from self.index.items()
346
+
347
+ def __len__(self) -> int:
348
+ """Return the number of rows contained in the DB.
349
+
350
+ Notes:
351
+ If more than one row has the same index value, it is only counted once.
352
+ """
353
+ return len(self.index)
354
+
355
+ def __contains__(self, key: str) -> bool:
356
+ """Return whether the given key is the index of a row in the DB."""
357
+ return key in self.index
358
+
359
+ def keys(self) -> Iterator[str]:
360
+ """Return a *view object* of the keys that are used as indices for the rows in the
361
+ DB.
362
+ """
363
+ yield from self.index.keys()
364
+
365
+ def items(self) -> Iterator[Tuple[str, Any]]:
366
+ """Return a *view object* of the items (``(index, row)`` pairs) that are used as indices
367
+ for the rows in the DB.
368
+ """
369
+ yield from self.index.items()
370
+
371
+ def export(self) -> Dict[str, Any]:
372
+ """Export the object to a dict, where each item in the dictionary
373
+ has an index as the key, and the value of the DB row associated with that index
374
+ as a value, represented itself as a :class:`dict` (as returned by :func:`DB_Row.export`).
375
+
376
+ The outer dictionary contains the rows in the physical order they are contained in
377
+ memory.
378
+
379
+ Notes:
380
+ This function effectively returns a snapshot of the DB.
381
+ """
382
+ ret = {}
383
+ for k, v in self.items():
384
+ ret[k] = v.export()
385
+ return ret
386
+
387
+ def set_data(self, bytearray_: bytearray) -> None:
388
+ """Set the new buffer data from the PLC to the current instance.
389
+
390
+ Args:
391
+ bytearray_: buffer to save.
392
+
393
+ Raises:
394
+ :obj:`TypeError`: if `bytearray_` is not an instance of :obj:`bytearray`
395
+ """
396
+ if not isinstance(bytearray_, bytearray):
397
+ raise TypeError(f"Value bytearray_: {bytearray_} is not from type bytearray")
398
+ self._bytearray = bytearray_
399
+
400
+ def read(self, client: Client) -> None:
401
+ """Reads all the rows from the PLC to the :obj:`bytearray` of this instance.
402
+
403
+ Args:
404
+ client: :obj:`Client` snap7 instance.
405
+
406
+ Raises:
407
+ :obj:`ValueError`: if the `row_size` is less than 0.
408
+ """
409
+ if self.row_size < 0:
410
+ raise ValueError("row_size must be greater equal zero.")
411
+
412
+ total_size = self.size * (self.row_size + self.row_offset)
413
+ if self.area == Area.DB: # note: is it worth using the upload method?
414
+ bytearray_ = client.db_read(self.db_number, self.db_offset, total_size)
415
+ else:
416
+ bytearray_ = client.read_area(self.area, 0, self.db_offset, total_size)
417
+
418
+ # replace data in bytearray
419
+ for i, b in enumerate(bytearray_):
420
+ self._bytearray[i + self.db_offset] = b
421
+
422
+ self.index.clear()
423
+ self.make_rows()
424
+
425
+ def write(self, client: Client) -> None:
426
+ """Writes all the rows from the :obj:`bytearray` of this instance to the PLC
427
+
428
+ Notes:
429
+ When the row_offset property has been set to something other than None while
430
+ constructing this object, this operation is not guaranteed to be atomic.
431
+
432
+ Args:
433
+ client: :obj:`Client` snap7 instance.
434
+
435
+ Raises:
436
+ :obj:`ValueError`: if the `row_size` is less than 0.
437
+ """
438
+ if self.row_size < 0:
439
+ raise ValueError("row_size must be greater equal zero.")
440
+
441
+ # special case: we have a row offset, so we must write each row individually
442
+ # this is because we don't want to change the data before the offset
443
+ if self.row_offset:
444
+ for _, v in self.index.items():
445
+ v.write(client)
446
+ return
447
+
448
+ total_size = self.size * (self.row_size + self.row_offset)
449
+ data = self._bytearray[self.db_offset : self.db_offset + total_size]
450
+
451
+ if self.area == Area.DB:
452
+ client.db_write(self.db_number, self.db_offset, data)
453
+ else:
454
+ client.write_area(self.area, 0, self.db_offset, data)
455
+
456
+ def get_bytearray(self) -> bytearray:
457
+ return self._bytearray
458
+
459
+
460
+ class Row:
461
+ """
462
+ Provide ROW API for DB bytearray
463
+
464
+ Attributes:
465
+ bytearray_: reference to the data of the parent DB.
466
+ _specification: row specification layout.
467
+ """
468
+
469
+ bytearray_: bytearray # data of reference to parent DB
470
+ _specification: Dict[str, Any] = {} # row specification
471
+
472
+ def __init__(
473
+ self,
474
+ bytearray_: Union[bytearray, "DB"],
475
+ _specification: str,
476
+ row_size: int = 0,
477
+ db_offset: int = 0,
478
+ layout_offset: int = 0,
479
+ row_offset: Optional[int] = 0,
480
+ area: Area = Area.DB,
481
+ ):
482
+ """Creates a new instance of the `DB_Row` class.
483
+
484
+ Args:
485
+ bytearray_: reference to the data of the parent DB.
486
+ _specification: row specification layout.
487
+ row_size: Amount of bytes of the row.
488
+ db_offset: at which byte in the db starts reading.
489
+ layout_offset: at which byte in the row specification we
490
+ start reading the data.
491
+ row_offset: offset between rows.
492
+ area: which memory area this row is representing.
493
+
494
+ Raises:
495
+ :obj:`TypeError`: if `bytearray_` is not an instance of :obj:`bytearray` or :obj:`DB`.
496
+ """
497
+
498
+ self.db_offset = db_offset # start point of row data in db
499
+ self.layout_offset = layout_offset # start point of row data in layout
500
+ self.row_size = row_size # length of the read
501
+ self.row_offset = row_offset # start of writable part of row
502
+ self.area = area
503
+
504
+ if not isinstance(bytearray_, (bytearray, DB)):
505
+ raise TypeError(f"Value bytearray_ {bytearray_} is not from type (bytearray, DB)")
506
+ self._bytearray = bytearray_
507
+ self._specification = parse_specification(_specification)
508
+
509
+ def get_bytearray(self) -> bytearray:
510
+ """Gets bytearray from self or DB parent
511
+
512
+ Returns:
513
+ Buffer data corresponding to the row.
514
+ """
515
+ if isinstance(self._bytearray, DB):
516
+ return self._bytearray.get_bytearray()
517
+ return self._bytearray
518
+
519
+ def export(self) -> Dict[str, Union[str, int, float, bool, datetime]]:
520
+ """Export dictionary with values
521
+
522
+ Returns:
523
+ dictionary containing the values of each value of the row.
524
+ """
525
+ return {key: self[key] for key in self._specification}
526
+
527
+ def __getitem__(self, key: str) -> Any:
528
+ """
529
+ Get a specific db field
530
+ """
531
+ index, _type = self._specification[key]
532
+ return self.get_value(index, _type)
533
+
534
+ def __setitem__(self, key: str, value: Any) -> None:
535
+ index, _type = self._specification[key]
536
+ self.set_value(index, _type, value)
537
+
538
+ def __repr__(self) -> str:
539
+ string = ""
540
+ for var_name, (index, _type) in self._specification.items():
541
+ string = f"{string}\n{var_name:<20} {self.get_value(index, _type)!r:<10}"
542
+ return string
543
+
544
+ def unchanged(self, bytearray_: bytearray) -> bool:
545
+ """Checks if the bytearray is the same
546
+
547
+ Args:
548
+ bytearray_: buffer of data to check.
549
+
550
+ Returns:
551
+ True if the current `bytearray_` is equal to the new one. Otherwise, this is False.
552
+ """
553
+ return self.get_bytearray() == bytearray_
554
+
555
+ def get_offset(self, byte_index: Union[str, int]) -> int:
556
+ """Calculate correct beginning position for a row
557
+ the db_offset = row_size * index
558
+
559
+ Args:
560
+ byte_index: byte index from where to start reading from.
561
+
562
+ Returns:
563
+ Amount of bytes to ignore.
564
+ """
565
+ # add float typ to avoid error because of
566
+ # the variable address with decimal point(like 0.0 or 4.0)
567
+ return int(float(byte_index)) - self.layout_offset + self.db_offset
568
+
569
+ def get_value(self, byte_index: Union[str, int], type_: str) -> ValueType:
570
+ """Gets the value for a specific type.
571
+
572
+ Args:
573
+ byte_index: byte index from where start reading.
574
+ type_: type of data to read.
575
+
576
+ Raises:
577
+ :obj:`ValueError`: if reading a `string` when checking the length of the string.
578
+ :obj:`ValueError`: if the `type_` is not handled.
579
+
580
+ Returns:
581
+ Value read according to the `type_`
582
+ """
583
+ bytearray_ = self.get_bytearray()
584
+
585
+ # set parsing non case-sensitive
586
+ type_ = type_.upper()
587
+
588
+ if type_ == "BOOL":
589
+ byte_index, bool_index = str(byte_index).split(".")
590
+ return get_bool(bytearray_, self.get_offset(byte_index), int(bool_index))
591
+
592
+ # remove 4 from byte index since
593
+ # first 4 bytes are used by db
594
+ byte_index = self.get_offset(byte_index)
595
+
596
+ if type_.startswith("FSTRING"):
597
+ max_size = re.search(r"\d+", type_)
598
+ if max_size is None:
599
+ raise ValueError("Max size could not be determinate. re.search() returned None")
600
+ return get_fstring(bytearray_, byte_index, int(max_size[0]))
601
+ elif type_.startswith("STRING"):
602
+ max_size = re.search(r"\d+", type_)
603
+ if max_size is None:
604
+ raise ValueError("Max size could not be determinate. re.search() returned None")
605
+ return get_string(bytearray_, byte_index)
606
+ elif type_.startswith("WSTRING"):
607
+ max_size = re.search(r"\d+", type_)
608
+ if max_size is None:
609
+ raise ValueError("Max size could not be determinate. re.search() returned None")
610
+ return get_wstring(bytearray_, byte_index)
611
+ else:
612
+ type_to_func: Dict[str, Callable[[bytearray, int], ValueType]] = {
613
+ "REAL": get_real,
614
+ "DWORD": get_dword,
615
+ "UDINT": get_udint,
616
+ "DINT": get_dint,
617
+ "UINT": get_uint,
618
+ "INT": get_int,
619
+ "WORD": get_word,
620
+ "BYTE": get_byte,
621
+ "S5TIME": get_s5time,
622
+ "DATE_AND_TIME": get_dt,
623
+ "USINT": get_usint,
624
+ "SINT": get_sint,
625
+ "TIME": get_time,
626
+ "DATE": get_date,
627
+ "TIME_OF_DAY": get_tod,
628
+ "LREAL": get_lreal,
629
+ "TOD": get_tod,
630
+ "CHAR": get_char,
631
+ "WCHAR": get_wchar,
632
+ "DTL": get_dtl,
633
+ }
634
+ if type_ in type_to_func:
635
+ return type_to_func[type_](bytearray_, byte_index)
636
+ raise ValueError
637
+
638
+ def set_value(self, byte_index: Union[str, int], type_: str, value: Union[bool, str, float]) -> Optional[bytearray]:
639
+ """Sets the value for a specific type in the specified byte index.
640
+
641
+ Args:
642
+ byte_index: byte index to start writing to.
643
+ type_: type of value to write.
644
+ value: value to write.
645
+
646
+ Raises:
647
+ :obj:`ValueError`: if reading a `string` when checking the length of the string.
648
+ :obj:`ValueError`: if the `type_` is not handled.
649
+
650
+ Returns:
651
+ Buffer data with the value written. Optional.
652
+ """
653
+
654
+ bytearray_ = self.get_bytearray()
655
+
656
+ if type_ == "BOOL" and isinstance(value, bool):
657
+ byte_index, bool_index = str(byte_index).split(".")
658
+ return set_bool(bytearray_, self.get_offset(byte_index), int(bool_index), value)
659
+
660
+ byte_index = self.get_offset(byte_index)
661
+
662
+ if type_.startswith("FSTRING") and isinstance(value, str):
663
+ max_size = re.search(r"\d+", type_)
664
+ if max_size is None:
665
+ raise ValueError("Max size could not be determinate. re.search() returned None")
666
+ max_size_grouped = max_size.group(0)
667
+ max_size_int = int(max_size_grouped)
668
+ set_fstring(bytearray_, byte_index, value, max_size_int)
669
+ return None
670
+
671
+ if type_.startswith("STRING") and isinstance(value, str):
672
+ max_size = re.search(r"\d+", type_)
673
+ if max_size is None:
674
+ raise ValueError("Max size could not be determinate. re.search() returned None")
675
+ max_size_grouped = max_size.group(0)
676
+ max_size_int = int(max_size_grouped)
677
+ set_string(bytearray_, byte_index, value, max_size_int)
678
+ return None
679
+
680
+ if type_.startswith("WSTRING") and isinstance(value, str):
681
+ max_size = re.search(r"\d+", type_)
682
+ if max_size is None:
683
+ raise ValueError("Max size could not be determinate. re.search() returned None")
684
+ max_size_int = int(max_size.group(0))
685
+ set_wstring(bytearray_, byte_index, value, max_size_int)
686
+ return None
687
+
688
+ if type_ == "REAL":
689
+ return set_real(bytearray_, byte_index, value)
690
+
691
+ if type_ == "LREAL" and isinstance(value, float):
692
+ return set_lreal(bytearray_, byte_index, value)
693
+
694
+ if type_ == "CHAR" and isinstance(value, str):
695
+ return set_char(bytearray_, byte_index, value)
696
+
697
+ if type_ == "WCHAR" and isinstance(value, str):
698
+ return set_wchar(bytearray_, byte_index, value)
699
+
700
+ if isinstance(value, int):
701
+ type_to_func = {
702
+ "DWORD": set_dword,
703
+ "UDINT": set_udint,
704
+ "DINT": set_dint,
705
+ "UINT": set_uint,
706
+ "INT": set_int,
707
+ "WORD": set_word,
708
+ "BYTE": set_byte,
709
+ "USINT": set_usint,
710
+ "SINT": set_sint,
711
+ }
712
+ if type_ in type_to_func:
713
+ return type_to_func[type_](bytearray_, byte_index, value)
714
+
715
+ if type_ == "TIME" and isinstance(value, str):
716
+ return set_time(bytearray_, byte_index, value)
717
+
718
+ if type_ == "DATE" and isinstance(value, date):
719
+ return set_date(bytearray_, byte_index, value)
720
+
721
+ if type_ in ("TIME_OF_DAY", "TOD") and isinstance(value, timedelta):
722
+ return set_tod(bytearray_, byte_index, value)
723
+
724
+ if type_ == "DTL" and isinstance(value, datetime):
725
+ return set_dtl(bytearray_, byte_index, value)
726
+
727
+ if type_ == "DATE_AND_TIME" and isinstance(value, datetime):
728
+ return set_dt(bytearray_, byte_index, value)
729
+
730
+ raise ValueError
731
+
732
+ def write(self, client: Client) -> None:
733
+ """Write current data to db in plc
734
+
735
+ Args:
736
+ client: :obj:`Client` snap7 instance.
737
+
738
+ Raises:
739
+ :obj:`TypeError`: if the `_bytearray` is not an instance of :obj:`DB` class.
740
+ :obj:`ValueError`: if the `row_size` is less than 0.
741
+ """
742
+ if not isinstance(self._bytearray, DB):
743
+ raise TypeError(f"Value self._bytearray: {self._bytearray} is not from type DB.")
744
+ if self.row_size < 0:
745
+ raise ValueError("row_size must be greater equal zero.")
746
+
747
+ db_nr = self._bytearray.db_number
748
+ offset = self.db_offset
749
+ data = self.get_bytearray()[offset : offset + self.row_size]
750
+ db_offset = self.db_offset
751
+
752
+ # indicate start of write only area of row!
753
+ if self.row_offset:
754
+ data = data[self.row_offset :]
755
+ db_offset += self.row_offset
756
+
757
+ if self.area == Area.DB:
758
+ client.db_write(db_nr, db_offset, data)
759
+ else:
760
+ client.write_area(self.area, 0, db_offset, data)
761
+
762
+ def read(self, client: Client) -> None:
763
+ """Read current data of db row from plc.
764
+
765
+ Args:
766
+ client: :obj:`Client` snap7 instance.
767
+
768
+ Raises:
769
+ :obj:`TypeError`: if the `_bytearray` is not an instance of :obj:`DB` class.
770
+ :obj:`ValueError`: if the `row_size` is less than 0.
771
+ """
772
+ if not isinstance(self._bytearray, DB):
773
+ raise TypeError(f"Value self._bytearray:{self._bytearray} is not from type DB.")
774
+ if self.row_size < 0:
775
+ raise ValueError("row_size must be greater equal zero.")
776
+ db_nr = self._bytearray.db_number
777
+ if self.area == Area.DB:
778
+ bytearray_ = client.db_read(db_nr, self.db_offset, self.row_size)
779
+ else:
780
+ bytearray_ = client.read_area(self.area, 0, self.db_offset, self.row_size)
781
+
782
+ data = self.get_bytearray()
783
+ # replace data in bytearray
784
+ for i, b in enumerate(bytearray_):
785
+ data[i + self.db_offset] = b
786
+
787
+
788
+ # backwards compatible alias
789
+ DB_Row = Row