switch-api 0.6.15__tar.gz → 0.6.17__tar.gz

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.
Files changed (70) hide show
  1. {switch_api-0.6.15 → switch_api-0.6.17}/HISTORY.md +13 -0
  2. {switch_api-0.6.15 → switch_api-0.6.17}/PKG-INFO +14 -1
  3. {switch_api-0.6.15 → switch_api-0.6.17}/setup.py +1 -1
  4. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/__init__.py +1 -1
  5. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/controls/controls.py +114 -44
  6. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/integration/integration.py +3 -1
  7. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api.egg-info/PKG-INFO +14 -1
  8. {switch_api-0.6.15 → switch_api-0.6.17}/AUTHORS.rst +0 -0
  9. {switch_api-0.6.15 → switch_api-0.6.17}/LICENCE +0 -0
  10. {switch_api-0.6.15 → switch_api-0.6.17}/MANIFEST.in +0 -0
  11. {switch_api-0.6.15 → switch_api-0.6.17}/README.md +0 -0
  12. {switch_api-0.6.15 → switch_api-0.6.17}/pyproject.toml +0 -0
  13. {switch_api-0.6.15 → switch_api-0.6.17}/setup.cfg +0 -0
  14. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_authentication/__init__.py +0 -0
  15. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_authentication/_authentication.py +0 -0
  16. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_authentication/_credentials_store/__init__.py +0 -0
  17. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_authentication/_credentials_store/_credentials_store.py +0 -0
  18. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_authentication/_msal/__init__.py +0 -0
  19. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_authentication/_msal/_custom_application.py +0 -0
  20. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_compute/__init__.py +0 -0
  21. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_compute/_commands.py +0 -0
  22. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_compute/_compute.py +0 -0
  23. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_compute/_iotservice.py +0 -0
  24. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_compute/_models.py +0 -0
  25. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_compute/_utils.py +0 -0
  26. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_guide/__init__.py +0 -0
  27. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_guide/main.py +0 -0
  28. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_guide/processor.py +0 -0
  29. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_utils/__init__.py +0 -0
  30. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_utils/_constants.py +0 -0
  31. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_utils/_marketplace.py +0 -0
  32. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_utils/_mqtt.py +0 -0
  33. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_utils/_platform.py +0 -0
  34. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/_utils/_utils.py +0 -0
  35. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/analytics/__init__.py +0 -0
  36. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/analytics/analytics.py +0 -0
  37. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/cache/__init__.py +0 -0
  38. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/cache/cache.py +0 -0
  39. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/controls/__init__.py +0 -0
  40. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/controls/_constants.py +0 -0
  41. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/controls/_enums.py +0 -0
  42. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/controls/_mqtt.py +0 -0
  43. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/dataset/__init__.py +0 -0
  44. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/dataset/dataset.py +0 -0
  45. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/email/__init__.py +0 -0
  46. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/email/email_sender.py +0 -0
  47. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/error_handlers/__init__.py +0 -0
  48. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/error_handlers/error_handlers.py +0 -0
  49. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/extensions/__init__.py +0 -0
  50. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/extensions/extensions.py +0 -0
  51. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/extensions/field_meta.py +0 -0
  52. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/extensions/helpers.py +0 -0
  53. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/extensions/pipeline.py +0 -0
  54. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/initialize.py +0 -0
  55. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/integration/__init__.py +0 -0
  56. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/integration/_utils.py +0 -0
  57. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/integration/helpers.py +0 -0
  58. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/pipeline/__init__.py +0 -0
  59. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/pipeline/automation.py +0 -0
  60. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/pipeline/definitions.py +0 -0
  61. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/pipeline/pipeline.py +0 -0
  62. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/platform_insights/__init__.py +0 -0
  63. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/platform_insights/platform_insights.py +0 -0
  64. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/realtime/__init__.py +0 -0
  65. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/realtime/_mqtt.py +0 -0
  66. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api/realtime/realtime.py +0 -0
  67. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api.egg-info/SOURCES.txt +0 -0
  68. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api.egg-info/dependency_links.txt +0 -0
  69. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api.egg-info/requires.txt +0 -0
  70. {switch_api-0.6.15 → switch_api-0.6.17}/switch_api.egg-info/top_level.txt +0 -0
@@ -1,5 +1,18 @@
1
1
  # History
2
2
 
3
+ #0.6.17
4
+ ### Changed
5
+ In the `integration` module:
6
+ - Updated `upsert_sites()` to support `YearBuilt` and `Floors` as optional columns when upserting sites.
7
+ - Documented `YearBuilt` and `Floors` in the `upsert_sites()` accepted fields list.
8
+ - `get_sites()` now also returns `YearBuilt` and `Floors` columns.
9
+
10
+ #0.6.16
11
+ ### Changed
12
+ In the `controls` module:
13
+ - Updated `submit_control_continue()` to send log control requests to the IoT endpoint in chunks of 50 per request, improving reliability and preventing oversized payloads.
14
+ - Updated `submit_control_continue()` to also batch control requests sent via MQTT (50 per batch), ensuring more stable and efficient message delivery.
15
+
3
16
  #0.6.15
4
17
  ### Added
5
18
  In the `automation` module:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: switch_api
3
- Version: 0.6.15
3
+ Version: 0.6.17
4
4
  Summary: A complete package for data ingestion into the Switch Automation Platform.
5
5
  Home-page: UNKNOWN
6
6
  Author: Switch Automation Pty Ltd.
@@ -38,6 +38,19 @@ pip install switch_api
38
38
 
39
39
  # History
40
40
 
41
+ #0.6.17
42
+ ### Changed
43
+ In the `integration` module:
44
+ - Updated `upsert_sites()` to support `YearBuilt` and `Floors` as optional columns when upserting sites.
45
+ - Documented `YearBuilt` and `Floors` in the `upsert_sites()` accepted fields list.
46
+ - `get_sites()` now also returns `YearBuilt` and `Floors` columns.
47
+
48
+ #0.6.16
49
+ ### Changed
50
+ In the `controls` module:
51
+ - Updated `submit_control_continue()` to send log control requests to the IoT endpoint in chunks of 50 per request, improving reliability and preventing oversized payloads.
52
+ - Updated `submit_control_continue()` to also batch control requests sent via MQTT (50 per batch), ensuring more stable and efficient message delivery.
53
+
41
54
  #0.6.15
42
55
  ### Added
43
56
  In the `automation` module:
@@ -16,7 +16,7 @@ setup(
16
16
  long_description_content_type='text/markdown',
17
17
  license='MIT License',
18
18
  name="switch_api",
19
- version="0.6.15",
19
+ version="0.6.17",
20
20
  packages=find_packages(include=["switch_api", "switch_api.*"]),
21
21
  install_requires=['pandas==1.5.3', 'requests', 'azure-storage-blob', 'pandera[io]==0.7.1', 'azure-servicebus',
22
22
  'msal>=1.11.0', 'paho-mqtt==1.6.1', 'uvicorn==0.22.0', 'fastapi==0.98.0', 'pyodbc==4.0.39', 'cachetools==5.5.0',
@@ -42,4 +42,4 @@ from . import _guide
42
42
  # log.addHandler(_ch)
43
43
  # https://dev.to/joaomcteixeira/setting-up-python-logging-for-a-library-app-6ml
44
44
 
45
- __version__ = "0.6.15"
45
+ __version__ = "0.6.17"
@@ -55,6 +55,43 @@ global _session
55
55
  _control_components = {}
56
56
  _switch_mqtt = None
57
57
  _session = None
58
+ MAX_CONTROL_REQUEST_SENSORS = 50
59
+
60
+ def _chunk_dataframe(df: pandas.DataFrame, chunk_size: int = MAX_CONTROL_REQUEST_SENSORS) -> List[pandas.DataFrame]:
61
+ """Split a dataframe into bounded chunks while preserving row order."""
62
+ if df.empty:
63
+ return []
64
+
65
+ return [
66
+ df.iloc[start_idx:start_idx + chunk_size].copy()
67
+ for start_idx in range(0, len(df), chunk_size)
68
+ ]
69
+
70
+ def _build_chunk_session_ids(chunk_count: int, session_id: Union[uuid.UUID, str, None] = None) -> List[uuid.UUID]:
71
+ """Return one unique session id per chunk, reusing the provided id for the first chunk when valid."""
72
+ if chunk_count <= 0:
73
+ return []
74
+
75
+ chunk_session_ids: List[uuid.UUID] = []
76
+ seen_session_ids = set()
77
+
78
+ if is_valid_uuid(session_id):
79
+ first_session_id = uuid.UUID(str(session_id))
80
+ else:
81
+ first_session_id = uuid.uuid4()
82
+
83
+ chunk_session_ids.append(first_session_id)
84
+ seen_session_ids.add(str(first_session_id))
85
+
86
+ while len(chunk_session_ids) < chunk_count:
87
+ next_session_id = uuid.uuid4()
88
+ if str(next_session_id) in seen_session_ids:
89
+ continue
90
+
91
+ chunk_session_ids.append(next_session_id)
92
+ seen_session_ids.add(str(next_session_id))
93
+
94
+ return chunk_session_ids
58
95
 
59
96
  def submit_control_continue(api_inputs: ApiInputs, data: pandas.DataFrame, information_columns: list = [],
60
97
  is_send_control: bool = True, ws_timeout_in_seconds: int = WS_MQTT_CONNECTION_TIMEOUT):
@@ -409,39 +446,70 @@ def __send_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
409
446
  acknowledgement_df = format_acknowledgement_payload_df(
410
447
  df=acknowledgement_df, required_columns=required_columns, has_priority=has_priority)
411
448
 
449
+ control_request_df = control_request_df.copy()
450
+ control_request_df = format_control_payload_df(
451
+ required_columns=required_columns, has_priority=has_priority, df=control_request_df)
452
+
453
+ acknowledgement_chunks = _chunk_dataframe(acknowledgement_df)
454
+ control_request_chunks = _chunk_dataframe(control_request_df)
455
+ chunk_session_ids = _build_chunk_session_ids(
456
+ chunk_count=len(control_request_chunks),
457
+ session_id=session_id)
458
+
412
459
  if is_send_control:
413
- _, result_df = log_control_request(
414
- session=_session,
415
- api_inputs=api_inputs,
416
- df=acknowledgement_df,
417
- session_id=session_id,
418
- installation_id=installation_id)
419
- if result_df.empty:
460
+ if len(acknowledgement_chunks) != len(control_request_chunks):
461
+ logger.error("Chunk count mismatch between control acknowledgement and MQTT payloads.")
420
462
  df_matrix[COL_CONTROL_STATUS] = ControlStatus.NotSentToService.value
421
463
  return df_matrix
422
464
 
423
- control_request_df = control_request_df.copy()
424
- control_request_df = format_control_payload_df(
425
- required_columns=required_columns, has_priority=has_priority, df=control_request_df)
465
+ successful_ack_chunks = []
466
+ successful_control_chunks = []
467
+ successful_session_ids = []
468
+
469
+ for acknowledgement_chunk, control_request_chunk, chunk_session_id in zip(
470
+ acknowledgement_chunks,
471
+ control_request_chunks,
472
+ chunk_session_ids,
473
+ ):
474
+ _, result_df = log_control_request(
475
+ session=_session,
476
+ api_inputs=api_inputs,
477
+ df=acknowledgement_chunk,
478
+ session_id=chunk_session_id,
479
+ installation_id=installation_id,
480
+ )
426
481
 
427
- def process_paged_request(df, page_size: int = 20):
428
- paged_results = pandas.DataFrame()
429
- total_rows = len(df)
430
- num_pages = (total_rows + page_size - 1) // page_size
482
+ if result_df.empty:
483
+ logger.warning(
484
+ "Skipping failed chunk for session_id=%s",
485
+ chunk_session_id,
486
+ )
487
+ continue
431
488
 
432
- for page_num in range(num_pages):
433
- start_idx = page_num * page_size
434
- end_idx = min((page_num + 1) * page_size, total_rows)
489
+ successful_ack_chunks.append(acknowledgement_chunk)
490
+ successful_control_chunks.append(control_request_chunk)
491
+ successful_session_ids.append(chunk_session_id)
435
492
 
436
- # Get the data for the current page
437
- page_data = df.iloc[start_idx:end_idx]
493
+ #only successfuly chunks are used for the control request next
494
+ acknowledgement_chunks = successful_ack_chunks
495
+ control_request_chunks = successful_control_chunks
496
+ chunk_session_ids = successful_session_ids
438
497
 
439
- result = send_control(page_data)
498
+ if not acknowledgement_chunks:
499
+ logger.error("All chunks failed during log control request.")
500
+ df_matrix[COL_CONTROL_STATUS] = ControlStatus.NotSentToService.value
501
+ return df_matrix
502
+
503
+ def process_paged_request(chunks: List[pandas.DataFrame], chunk_session_ids: List[uuid.UUID]):
504
+ paged_results = pandas.DataFrame()
505
+
506
+ for page_data, chunk_session_id in zip(chunks, chunk_session_ids):
507
+ result = send_control(page_data, chunk_session_id)
440
508
  paged_results = pandas.concat([paged_results, result])
441
509
 
442
510
  return paged_results
443
511
 
444
- def send_control(page_data):
512
+ def send_control(page_data: pandas.DataFrame, chunk_session_id: uuid.UUID):
445
513
  retry_count = 0
446
514
  max_retries = 3
447
515
  success_results = pandas.DataFrame()
@@ -451,7 +519,7 @@ def __send_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
451
519
 
452
520
  while retry_count < max_retries:
453
521
  success_response, missing_response = _switch_mqtt.send_control_request(
454
- session_id=str(session_id),
522
+ session_id=str(chunk_session_id),
455
523
  installation_id=installation_id,
456
524
  sensors=dataframe_to_control.to_dict(orient='records'))
457
525
 
@@ -522,7 +590,7 @@ def __send_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
522
590
  return control_result
523
591
 
524
592
  if is_send_control:
525
- control_results = process_paged_request(control_request_df)
593
+ control_results = process_paged_request(control_request_chunks, chunk_session_ids)
526
594
  control_results = control_results.rename(
527
595
  columns={'sensorId': COL_OBJECT_PROPERTY_ID})
528
596
  control_results = pandas.merge(control_results, df_matrix[[
@@ -673,12 +741,18 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
673
741
  if _session == None:
674
742
  _session = requests_retry_session2(retries=5, backoff_factor=2)
675
743
 
676
- _, result_df = log_control_request(session=_session,
677
- api_inputs=api_inputs,
678
- df=data_frame,
679
- session_id=session_id,
680
- installation_id=installation_id)
681
- if result_df.empty:
744
+ acknowledgement_chunks = _chunk_dataframe(data_frame)
745
+ chunk_session_ids = _build_chunk_session_ids(
746
+ chunk_count=len(acknowledgement_chunks),
747
+ session_id=session_id)
748
+
749
+ for acknowledgement_chunk, chunk_session_id in zip(acknowledgement_chunks, chunk_session_ids):
750
+ _, result_df = log_control_request(session=_session,
751
+ api_inputs=api_inputs,
752
+ df=acknowledgement_chunk,
753
+ session_id=chunk_session_id,
754
+ installation_id=installation_id)
755
+ if result_df.empty:
682
756
  return pandas.DataFrame()
683
757
 
684
758
  # Proceeds when the control request is successful
@@ -722,24 +796,18 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
722
796
  logger.info("Could not connect to MQTT Broker.")
723
797
  return 'Could not connect to MQTT Broker.', pandas.DataFrame()
724
798
 
725
- def process_paged_request(df, page_size: int = 20):
726
- paged_results = pandas.DataFrame()
727
- total_rows = len(df)
728
- num_pages = (total_rows + page_size - 1) // page_size
729
-
730
- for page_num in range(num_pages):
731
- start_idx = page_num * page_size
732
- end_idx = min((page_num + 1) * page_size, total_rows)
799
+ control_request_chunks = _chunk_dataframe(data_frame)
733
800
 
734
- # Get the data for the current page
735
- page_data = df.iloc[start_idx:end_idx]
801
+ def process_paged_request(chunks: List[pandas.DataFrame], chunk_session_ids: List[uuid.UUID]):
802
+ paged_results = pandas.DataFrame()
736
803
 
737
- result = send_control(page_data)
804
+ for page_data, chunk_session_id in zip(chunks, chunk_session_ids):
805
+ result = send_control(page_data, chunk_session_id)
738
806
  paged_results = pandas.concat([paged_results, result])
739
807
 
740
808
  return paged_results
741
809
 
742
- def send_control(page_data):
810
+ def send_control(page_data: pandas.DataFrame, chunk_session_id: uuid.UUID):
743
811
  retry_count = 0
744
812
  max_retries = 3
745
813
  success_results = pandas.DataFrame()
@@ -749,7 +817,7 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
749
817
 
750
818
  while retry_count < max_retries:
751
819
  success_response, missing_response = _switch_mqtt.send_control_request(
752
- session_id=str(session_id),
820
+ session_id=str(chunk_session_id),
753
821
  installation_id=installation_id,
754
822
  sensors=dataframe_to_control.to_dict(orient='records'))
755
823
 
@@ -821,7 +889,7 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
821
889
 
822
890
  # [Subscribe] to ClientId + Site State Topics
823
891
  _switch_mqtt.subscribe_installation(installation_id=installation_id)
824
- control_results = process_paged_request(data_frame)
892
+ control_results = process_paged_request(control_request_chunks, chunk_session_ids)
825
893
 
826
894
  return control_results
827
895
 
@@ -983,6 +1051,8 @@ def log_control_request(session: requests.Session, api_inputs: ApiInputs, df: pa
983
1051
  logger.info("Control Request Session Id: %s", session_id)
984
1052
  logger.info("Control Request for User: %s=%s",
985
1053
  api_inputs.email_address, api_inputs.user_id)
1054
+ logger.info("Log Control Request Payload:")
1055
+ logger.info(json_payload)
986
1056
 
987
1057
  try:
988
1058
  response = session_post(session, url, json_payload, headers)
@@ -1177,4 +1247,4 @@ def session_post(session, url, json_payload, headers):
1177
1247
  if "Connection refused" in str(ex):
1178
1248
  logger.error("Hard failure: Connection Refused. Not retrying.")
1179
1249
  raise
1180
- raise
1250
+ raise
@@ -840,6 +840,8 @@ def upsert_sites(df: pandas.DataFrame, api_inputs: ApiInputs, tag_columns: list
840
840
  - Latitude
841
841
  - Longitude
842
842
  - Timezone
843
+ - YearBuilt
844
+ - Floors
843
845
  - InstallationId
844
846
  - The UUID of the existing site within the Switch Automation Platform.
845
847
 
@@ -871,7 +873,7 @@ def upsert_sites(df: pandas.DataFrame, api_inputs: ApiInputs, tag_columns: list
871
873
 
872
874
  required_columns = ['InstallationName', 'InstallationCode', 'Address', 'Country', 'Suburb', 'State', 'StateName',
873
875
  'FloorAreaM2', 'ZipPostCode']
874
- optional_columns = ['Latitude', 'Longitude', 'Timezone', 'InstallationId']
876
+ optional_columns = ['Latitude', 'Longitude', 'Timezone', 'InstallationId', 'YearBuilt', 'Floors']
875
877
  proposed_columns = data_frame.columns.tolist()
876
878
 
877
879
  if not set(required_columns).issubset(proposed_columns):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: switch-api
3
- Version: 0.6.15
3
+ Version: 0.6.17
4
4
  Summary: A complete package for data ingestion into the Switch Automation Platform.
5
5
  Home-page: UNKNOWN
6
6
  Author: Switch Automation Pty Ltd.
@@ -38,6 +38,19 @@ pip install switch_api
38
38
 
39
39
  # History
40
40
 
41
+ #0.6.17
42
+ ### Changed
43
+ In the `integration` module:
44
+ - Updated `upsert_sites()` to support `YearBuilt` and `Floors` as optional columns when upserting sites.
45
+ - Documented `YearBuilt` and `Floors` in the `upsert_sites()` accepted fields list.
46
+ - `get_sites()` now also returns `YearBuilt` and `Floors` columns.
47
+
48
+ #0.6.16
49
+ ### Changed
50
+ In the `controls` module:
51
+ - Updated `submit_control_continue()` to send log control requests to the IoT endpoint in chunks of 50 per request, improving reliability and preventing oversized payloads.
52
+ - Updated `submit_control_continue()` to also batch control requests sent via MQTT (50 per batch), ensuring more stable and efficient message delivery.
53
+
41
54
  #0.6.15
42
55
  ### Added
43
56
  In the `automation` module:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes