switch-api 0.6.12__tar.gz → 0.6.14__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.12 → switch_api-0.6.14}/HISTORY.md +16 -0
  2. {switch_api-0.6.12 → switch_api-0.6.14}/PKG-INFO +17 -1
  3. {switch_api-0.6.12 → switch_api-0.6.14}/setup.py +1 -1
  4. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/__init__.py +4 -2
  5. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_utils/_constants.py +6 -0
  6. switch_api-0.6.14/switch_api/_utils/_mqtt.py +124 -0
  7. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/controls/__init__.py +2 -3
  8. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/controls/_constants.py +0 -6
  9. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/controls/_mqtt.py +2 -3
  10. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/controls/controls.py +49 -116
  11. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/integration/integration.py +35 -16
  12. switch_api-0.6.14/switch_api/realtime/__init__.py +12 -0
  13. switch_api-0.6.14/switch_api/realtime/_mqtt.py +205 -0
  14. switch_api-0.6.14/switch_api/realtime/realtime.py +235 -0
  15. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api.egg-info/PKG-INFO +17 -1
  16. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api.egg-info/SOURCES.txt +5 -1
  17. {switch_api-0.6.12 → switch_api-0.6.14}/AUTHORS.rst +0 -0
  18. {switch_api-0.6.12 → switch_api-0.6.14}/LICENCE +0 -0
  19. {switch_api-0.6.12 → switch_api-0.6.14}/MANIFEST.in +0 -0
  20. {switch_api-0.6.12 → switch_api-0.6.14}/README.md +0 -0
  21. {switch_api-0.6.12 → switch_api-0.6.14}/pyproject.toml +0 -0
  22. {switch_api-0.6.12 → switch_api-0.6.14}/setup.cfg +0 -0
  23. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/__init__.py +0 -0
  24. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/_authentication.py +0 -0
  25. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/_credentials_store/__init__.py +0 -0
  26. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/_credentials_store/_credentials_store.py +0 -0
  27. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/_msal/__init__.py +0 -0
  28. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/_msal/_custom_application.py +0 -0
  29. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/__init__.py +0 -0
  30. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/_commands.py +0 -0
  31. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/_compute.py +0 -0
  32. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/_iotservice.py +0 -0
  33. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/_models.py +0 -0
  34. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/_utils.py +0 -0
  35. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_guide/__init__.py +0 -0
  36. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_guide/main.py +0 -0
  37. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_guide/processor.py +0 -0
  38. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_utils/__init__.py +0 -0
  39. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_utils/_marketplace.py +0 -0
  40. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_utils/_platform.py +0 -0
  41. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_utils/_utils.py +0 -0
  42. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/analytics/__init__.py +0 -0
  43. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/analytics/analytics.py +0 -0
  44. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/cache/__init__.py +0 -0
  45. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/cache/cache.py +0 -0
  46. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/controls/_enums.py +0 -0
  47. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/dataset/__init__.py +0 -0
  48. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/dataset/dataset.py +0 -0
  49. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/email/__init__.py +0 -0
  50. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/email/email_sender.py +0 -0
  51. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/error_handlers/__init__.py +0 -0
  52. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/error_handlers/error_handlers.py +0 -0
  53. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/extensions/__init__.py +0 -0
  54. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/extensions/extensions.py +0 -0
  55. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/extensions/field_meta.py +0 -0
  56. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/extensions/helpers.py +0 -0
  57. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/extensions/pipeline.py +0 -0
  58. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/initialize.py +0 -0
  59. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/integration/__init__.py +0 -0
  60. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/integration/_utils.py +0 -0
  61. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/integration/helpers.py +0 -0
  62. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/pipeline/__init__.py +0 -0
  63. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/pipeline/automation.py +0 -0
  64. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/pipeline/definitions.py +0 -0
  65. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/pipeline/pipeline.py +0 -0
  66. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/platform_insights/__init__.py +0 -0
  67. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/platform_insights/platform_insights.py +0 -0
  68. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api.egg-info/dependency_links.txt +0 -0
  69. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api.egg-info/requires.txt +0 -0
  70. {switch_api-0.6.12 → switch_api-0.6.14}/switch_api.egg-info/top_level.txt +0 -0
@@ -1,5 +1,21 @@
1
1
  # History
2
2
 
3
+ #0.6.14
4
+ ### Modified
5
+ In the `integration` module:
6
+ - Updated `add_carbon_column()` to enforce priority selection of carbon calculation rules.
7
+ - When overlapping date ranges exist for the same `ObjectPropertyId`, the rule with a non-null `ObjectPropertyTemplateID` is now preferred over the default (NULL) rule.
8
+ - Ensured only one carbon calculation is applied per input row to prevent duplicate records.
9
+ - Refactored logic to preserve original row count and avoid row loss during rule matching.
10
+
11
+ #0.6.13
12
+ ### Added
13
+ In the `controls` module
14
+ - Add `submit_realtime_updates` method to support realtime updates for a single dataset
15
+ - Subscribes to a single MQTT topic and retrieves retained message (if any)
16
+ - Merges retained dataset with provided DataFrame using Id-based override and append logic
17
+ - Publishes merged dataset back to the corresponding Realtime topic
18
+
3
19
  #0.6.12
4
20
  ### Modified
5
21
  In the `controls` module
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: switch_api
3
- Version: 0.6.12
3
+ Version: 0.6.14
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,22 @@ pip install switch_api
38
38
 
39
39
  # History
40
40
 
41
+ #0.6.14
42
+ ### Modified
43
+ In the `integration` module:
44
+ - Updated `add_carbon_column()` to enforce priority selection of carbon calculation rules.
45
+ - When overlapping date ranges exist for the same `ObjectPropertyId`, the rule with a non-null `ObjectPropertyTemplateID` is now preferred over the default (NULL) rule.
46
+ - Ensured only one carbon calculation is applied per input row to prevent duplicate records.
47
+ - Refactored logic to preserve original row count and avoid row loss during rule matching.
48
+
49
+ #0.6.13
50
+ ### Added
51
+ In the `controls` module
52
+ - Add `submit_realtime_updates` method to support realtime updates for a single dataset
53
+ - Subscribes to a single MQTT topic and retrieves retained message (if any)
54
+ - Merges retained dataset with provided DataFrame using Id-based override and append logic
55
+ - Publishes merged dataset back to the corresponding Realtime topic
56
+
41
57
  #0.6.12
42
58
  ### Modified
43
59
  In the `controls` 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.12",
19
+ version="0.6.14",
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',
@@ -10,7 +10,7 @@ Data Ingestion into the Switch Automation
10
10
  Complete package for ingestion data into the Switch Automation Platform.
11
11
  """
12
12
  __all__ = ['dataset', 'analytics', 'pipeline', 'error_handlers', 'integration',
13
- 'initialize', 'cache', 'extensions', 'email', 'controls', '_compute', 'generate_filepath']
13
+ 'initialize', 'cache', 'extensions', 'email', 'controls', 'realtime', '_compute', 'generate_filepath', 'set_mqtt_broker_config']
14
14
 
15
15
  from .initialize import initialize
16
16
  from . import dataset
@@ -23,8 +23,10 @@ from . import cache
23
23
  from . import extensions
24
24
  from . import email
25
25
  from . import controls
26
+ from . import realtime
26
27
  from . import _compute
27
28
  from ._utils._utils import generate_filepath
29
+ from ._utils._mqtt import set_mqtt_broker_config
28
30
  from . import _guide
29
31
 
30
32
 
@@ -40,4 +42,4 @@ from . import _guide
40
42
  # log.addHandler(_ch)
41
43
  # https://dev.to/joaomcteixeira/setting-up-python-logging-for-a-library-app-6ml
42
44
 
43
- __version__ = "0.6.12"
45
+ __version__ = "0.6.14"
@@ -94,3 +94,9 @@ GUIDES_EXTERNAL_TYPES = Literal['SwitchGuides']
94
94
  GUIDES_SCOPE = Literal['Portfolio-wide']
95
95
 
96
96
  IMPORT_STATUS = Literal['New', 'ToImport', 'OnHold', 'Exclude', 'Edited', 'ToRemove']
97
+
98
+ WS_DEFAULT_PORT = 443
99
+ WS_MQTT_CONNECTION_TIMEOUT = 180
100
+ WS_MQTT_DEFAULT_MAX_TIMEOUT = 180
101
+ WS_MQTT_WAIT_TIME_INTERVAL = 0.1
102
+ WS_MQTT_MESSAGE_WAIT_TIME_INTERVAL = 0.1
@@ -0,0 +1,124 @@
1
+ # -------------------------------------------------------------------------
2
+ # Copyright (c) Switch Automation Pty Ltd.
3
+ # Licensed under the MIT License.
4
+ # -------------------------------------------------------------------------
5
+ """
6
+ A module for mqtt related utils.
7
+ """
8
+
9
+ from .._utils._constants import (
10
+ WS_DEFAULT_PORT,
11
+ WS_MQTT_DEFAULT_MAX_TIMEOUT,
12
+ )
13
+
14
+ global _control_api_endpoint
15
+ global _control_ws_host
16
+ global _control_ws_port
17
+ global _control_ws_username
18
+ global _control_ws_password
19
+ global _control_ws_max_timeout
20
+
21
+ _control_api_endpoint = ''
22
+ _control_ws_host = ''
23
+ _control_ws_port = WS_DEFAULT_PORT
24
+ _control_ws_username = ''
25
+ _control_ws_password = ''
26
+ _control_ws_max_timeout = WS_MQTT_DEFAULT_MAX_TIMEOUT
27
+
28
+ def get_control_config():
29
+ """
30
+ Retrieve Control MQTT Configuration
31
+
32
+ Returns the currently configured MQTT control variables used by the
33
+ controls module when communicating with the MQTT Broker.
34
+
35
+ These values are typically set via `set_control_variables()` during
36
+ local execution. In production environments, they may be populated
37
+ from deployment environment variables.
38
+
39
+ Returns
40
+ -------
41
+ dict
42
+ A dictionary containing the current control configuration:
43
+
44
+ - api_endpoint : str
45
+ Platform IoT API endpoint.
46
+ - ws_host : str
47
+ WebSocket MQTT host URL (wss://).
48
+ - ws_port : int
49
+ MQTT broker port.
50
+ - username : str
51
+ Username for MQTT authentication.
52
+ - password : str
53
+ Password for MQTT authentication.
54
+ - max_timeout : int
55
+ Maximum timeout (in seconds) for control operations.
56
+ """
57
+ return {
58
+ "api_endpoint": _control_api_endpoint,
59
+ "ws_host": _control_ws_host,
60
+ "ws_port": _control_ws_port,
61
+ "username": _control_ws_username,
62
+ "password": _control_ws_password,
63
+ "max_timeout": _control_ws_max_timeout,
64
+ }
65
+
66
+
67
+ def set_mqtt_broker_config(api_endpoint: str, ws_host: str, user_name: str, password: str,
68
+ ws_port: int = WS_DEFAULT_PORT, max_timeout: int = WS_MQTT_DEFAULT_MAX_TIMEOUT, bypass_username_password: bool = False):
69
+ """Set Control Variables
70
+
71
+ Set Control Variables needed to enable control request to MQTT Broker when running locally.
72
+
73
+ In Production, these are pulled from the deployment environment variables.
74
+
75
+ Parameters
76
+ ----------
77
+ api_endpoint : str
78
+ Platform IoT API Endpoint.
79
+ host : str
80
+ Host URL for MQTT connection. This needs to be datacenter specfic URL.
81
+ port : int
82
+ MQTT message broker port. Defaults to 443.
83
+ user_name : str
84
+ Username for MQTT connection
85
+ password: str
86
+ Password for MQTT connection
87
+ max_timeout : int
88
+ Max timeout set for the controls module. Defaults to 30 seconds.
89
+ """
90
+ global _control_api_endpoint
91
+ global _control_ws_host
92
+ global _control_ws_port
93
+ global _control_ws_username
94
+ global _control_ws_password
95
+ global _control_ws_max_timeout
96
+
97
+ # Check if endpoint is a valid URL
98
+ if not api_endpoint.startswith('https://'):
99
+ raise ValueError(
100
+ "Invalid IoT API Endpoint. The IoT host should start with 'https://'.")
101
+
102
+ # # Check if host is a valid URL
103
+ if not ws_host.startswith('wss://'):
104
+ raise ValueError(
105
+ "Invalid IoT Websocket MQTT Host. The IoT host should start with 'wss://'.")
106
+
107
+ # Check if user_name and password are not empty
108
+ if not bypass_username_password:
109
+ if not user_name:
110
+ raise ValueError("user_name cannot be empty.")
111
+ if not password:
112
+ raise ValueError("password cannot be empty.")
113
+
114
+ # Check if max_timeout is greated than 0
115
+ if max_timeout < 1:
116
+ raise ValueError("max_timeout should be greater than 0.")
117
+
118
+ # Set global variables
119
+ _control_api_endpoint = api_endpoint
120
+ _control_ws_host = ws_host
121
+ _control_ws_port = ws_port
122
+ _control_ws_username = user_name
123
+ _control_ws_password = password
124
+ _control_ws_max_timeout = max_timeout
@@ -7,7 +7,6 @@
7
7
  A module for sending control request of sensors.
8
8
  """
9
9
 
10
- from .controls import submit_control, set_control_variables, submit_control_continue, add_control_component
10
+ __all__ = ['submit_control', 'submit_control_continue', 'add_control_component']
11
11
 
12
- __all__ = ['submit_control', 'set_control_variables',
13
- 'submit_control_continue', 'add_control_component']
12
+ from .controls import submit_control, submit_control_continue, add_control_component
@@ -11,12 +11,6 @@ is not directly referenced by end users.
11
11
  IOT_RESPONSE_SUCCESS = 'success'
12
12
  IOT_RESPONSE_ERROR = 'error'
13
13
 
14
- WS_DEFAULT_PORT = 443
15
- WS_MQTT_CONNECTION_TIMEOUT = 180
16
- WS_MQTT_DEFAULT_MAX_TIMEOUT = 30
17
- WS_MQTT_WAIT_TIME_INTERVAL = 0.1
18
- WS_MQTT_MESSAGE_WAIT_TIME_INTERVAL = 0.1
19
-
20
14
  CONTROL_REQUEST_ACTION_ACK = 'control-request-ack'
21
15
  CONTROL_REQUEST_ACTION_RESULT = 'control-result'
22
16
 
@@ -10,14 +10,13 @@ module is not directly referenced by end users.
10
10
  import json
11
11
  import logging
12
12
  import sys
13
- import time
14
13
  import threading
15
14
  from urllib.parse import urlparse
16
15
  import paho.mqtt.client as mqtt
17
16
  import pandas
18
17
  from paho.mqtt.client import MQTTMessage
19
- from ._constants import (CONTROL_REQUEST_ACTION_ACK, CONTROL_REQUEST_ACTION_RESULT, WS_DEFAULT_PORT,
20
- WS_MQTT_CONNECTION_TIMEOUT, WS_MQTT_WAIT_TIME_INTERVAL)
18
+ from ._constants import (CONTROL_REQUEST_ACTION_ACK)
19
+ from .._utils._constants import (WS_DEFAULT_PORT, WS_MQTT_CONNECTION_TIMEOUT)
21
20
 
22
21
  logger = logging.getLogger(__name__)
23
22
  logger.setLevel(logging.DEBUG)
@@ -9,51 +9,45 @@ A module for sending control request of sensors.
9
9
 
10
10
  import json
11
11
  import logging
12
- import os
13
12
  import sys
14
13
  import time
15
- from typing import Dict, List, Union, Optional
14
+ from typing import List, Union
16
15
  import uuid
17
16
  import pandas
18
17
  import requests
19
- from requests.adapters import HTTPAdapter
20
- from urllib3.util.retry import Retry
21
18
  import requests
22
19
 
23
20
  from ._enums import ControlStatus
24
- from ._constants import (IOT_RESPONSE_ERROR, IOT_RESPONSE_SUCCESS, WS_DEFAULT_PORT, WS_MQTT_CONNECTION_TIMEOUT,
25
- WS_MQTT_DEFAULT_MAX_TIMEOUT, COL_CONTROL_DEFAULT_CONTROL_VALUE, COL_OBJECT_PROPERTY_ID, COL_CONTROL_NAME,
21
+ from ._constants import (IOT_RESPONSE_ERROR, IOT_RESPONSE_SUCCESS, COL_CONTROL_DEFAULT_CONTROL_VALUE, COL_OBJECT_PROPERTY_ID, COL_CONTROL_NAME,
26
22
  COL_CONTROL_LABEL, COL_CONTROL_VALUE, COL_CONTROL_CONTINUE_VALUE, COL_CONTROL_TTL, COL_CONTROL_PRIORITY,
27
23
  COL_CONTROL_INSTALLATION_ID, COL_CONTROL_STATUS, COL_CONTROL_TIMESTAMP, COL_CONTROL_TIMESTAMP_LOCAL,
28
24
  COL_CONTROL_PIVOTKEY, COL_CONTROL_INFOCOLUMNS, COL_CONTROL_IS_EXTENDED_CONTROL, COL_CONTROL_EXISTS_IN_CONTROL_CACHE)
29
25
  from ._mqtt import SwitchMQTT
26
+ from .._utils import _mqtt
27
+ from .._utils._constants import WS_MQTT_CONNECTION_TIMEOUT
30
28
  from .._utils._utils import ApiInputs, _with_func_attrs, is_valid_uuid, requests_retry_session2
31
29
  from ..cache.cache import get_cache, set_cache
32
30
 
31
+ # -----------------------------------------------------------------------------
32
+ # Logging Setup
33
+ # -----------------------------------------------------------------------------
33
34
  logger = logging.getLogger(__name__)
34
35
  logger.setLevel(logging.DEBUG)
35
- consoleHandler = logging.StreamHandler(stream=sys.stdout)
36
- consoleHandler.setLevel(logging.INFO)
37
-
38
- logger.addHandler(consoleHandler)
39
- formatter = logging.Formatter('%(asctime)s %(name)s.%(funcName)s %(levelname)s: %(message)s',
40
- datefmt='%Y-%m-%dT%H:%M:%S')
41
- consoleHandler.setFormatter(formatter)
42
-
43
- global _control_api_endpoint
44
- global _control_ws_host
45
- global _control_ws_port
46
- global _control_ws_username
47
- global _control_ws_password
48
- global _control_ws_max_timeout
49
-
50
- _control_api_endpoint = ''
51
- _control_ws_host = ''
52
- _control_ws_port = WS_DEFAULT_PORT
53
- _control_ws_username = ''
54
- _control_ws_password = ''
55
- _control_ws_max_timeout = WS_MQTT_DEFAULT_MAX_TIMEOUT
56
36
 
37
+ if not logger.handlers:
38
+ console_handler = logging.StreamHandler(stream=sys.stdout)
39
+ console_handler.setLevel(logging.INFO)
40
+
41
+ formatter = logging.Formatter(
42
+ "%(asctime)s %(name)s.%(funcName)s %(levelname)s: %(message)s",
43
+ datefmt="%Y-%m-%dT%H:%M:%S",
44
+ )
45
+ console_handler.setFormatter(formatter)
46
+ logger.addHandler(console_handler)
47
+
48
+ # -----------------------------------------------------------------------------
49
+ # Globals
50
+ # -----------------------------------------------------------------------------
57
51
  global _control_components
58
52
  global _switch_mqtt
59
53
  global _session
@@ -62,65 +56,6 @@ _control_components = {}
62
56
  _switch_mqtt = None
63
57
  _session = None
64
58
 
65
- def set_control_variables(api_endpoint: str, ws_host: str, user_name: str, password: str,
66
- ws_port: int = WS_DEFAULT_PORT, max_timeout: int = WS_MQTT_DEFAULT_MAX_TIMEOUT, bypass_username_password: bool = False):
67
- """Set Control Variables
68
-
69
- Set Control Variables needed to enable control request to MQTT Broker when running locally.
70
-
71
- In Production, these are pulled from the deployment environment variables.
72
-
73
- Parameters
74
- ----------
75
- api_endpoint : str
76
- Platform IoT API Endpoint.
77
- host : str
78
- Host URL for MQTT connection. This needs to be datacenter specfic URL.
79
- port : int
80
- MQTT message broker port. Defaults to 443.
81
- user_name : str
82
- Username for MQTT connection
83
- password: str
84
- Password for MQTT connection
85
- max_timeout : int
86
- Max timeout set for the controls module. Defaults to 30 seconds.
87
- """
88
- global _control_api_endpoint
89
- global _control_ws_host
90
- global _control_ws_port
91
- global _control_ws_username
92
- global _control_ws_password
93
- global _control_ws_max_timeout
94
-
95
- # Check if endpoint is a valid URL
96
- if not api_endpoint.startswith('https://'):
97
- raise ValueError(
98
- "Invalid IoT API Endpoint. The IoT host should start with 'https://'.")
99
-
100
- # # Check if host is a valid URL
101
- if not ws_host.startswith('wss://'):
102
- raise ValueError(
103
- "Invalid IoT Websocket MQTT Host. The IoT host should start with 'wss://'.")
104
-
105
- # Check if user_name and password are not empty
106
- if not bypass_username_password:
107
- if not user_name:
108
- raise ValueError("user_name cannot be empty.")
109
- if not password:
110
- raise ValueError("password cannot be empty.")
111
-
112
- # Check if max_timeout is greated than 0
113
- if max_timeout < 1:
114
- raise ValueError("max_timeout should be greater than 0.")
115
-
116
- # Set global variables
117
- _control_api_endpoint = api_endpoint
118
- _control_ws_host = ws_host
119
- _control_ws_port = ws_port
120
- _control_ws_username = user_name
121
- _control_ws_password = password
122
- _control_ws_max_timeout = max_timeout
123
-
124
59
  def submit_control_continue(api_inputs: ApiInputs, data: pandas.DataFrame, information_columns: list = [],
125
60
  is_send_control: bool = True, ws_timeout_in_seconds: int = WS_MQTT_CONNECTION_TIMEOUT):
126
61
  """Submit control of sensor(s)
@@ -143,17 +78,16 @@ def submit_control_continue(api_inputs: ApiInputs, data: pandas.DataFrame, infor
143
78
  When value is set to 0 it defaults to max timeout value.
144
79
  When value is above max timeout value it defaults to max timeout value.
145
80
 
146
-
147
81
  Returns
148
82
  -------
149
83
  Dataframe
150
84
  control_response = is the list of sensors that were controlled or not
151
85
  """
152
86
  global _control_components
153
- global _control_ws_max_timeout
154
87
  global _switch_mqtt
155
88
  global _session
156
-
89
+
90
+ mqtt_config = _mqtt.get_control_config()
157
91
  control_results = []
158
92
 
159
93
  # Validate Information Columns if existing
@@ -168,26 +102,26 @@ def submit_control_continue(api_inputs: ApiInputs, data: pandas.DataFrame, infor
168
102
 
169
103
  if ws_timeout_in_seconds < 0:
170
104
  logger.error(
171
- f"Invalid timeout value. Timeout should be between 0 and {_control_ws_max_timeout}. Setting to zero will default to max timeout.")
105
+ f"Invalid timeout value. Timeout should be between 0 and {mqtt_config['max_timeout']}. Setting to zero will default to max timeout.")
172
106
  return pandas.DataFrame()
173
107
 
174
- if ws_timeout_in_seconds > _control_ws_max_timeout:
108
+ if ws_timeout_in_seconds > mqtt_config["max_timeout"]:
175
109
  logger.critical(
176
110
  f'Timeout is greater than Max Timeout value. Setting timeout to Max Timeout Value instead.')
177
- ws_timeout_in_seconds = _control_ws_max_timeout
111
+ ws_timeout_in_seconds = mqtt_config["max_timeout"]
178
112
 
179
113
  if ws_timeout_in_seconds == 0:
180
- ws_timeout_in_seconds = _control_ws_max_timeout
114
+ ws_timeout_in_seconds = mqtt_config["max_timeout"]
181
115
 
182
116
  # [Create] SwitchMQTT
183
117
  if _switch_mqtt == None:
184
- _switch_mqtt = SwitchMQTT(host_address=_control_ws_host,
185
- host_port=_control_ws_port,
186
- username=_control_ws_username,
187
- password=_control_ws_password,
188
- client_id=api_inputs.data_feed_id,
189
- email=api_inputs.email_address,
190
- project_id=api_inputs.api_project_id)
118
+ _switch_mqtt = SwitchMQTT(host_address=mqtt_config["ws_host"],
119
+ host_port=mqtt_config["ws_port"],
120
+ username=mqtt_config["username"],
121
+ password=mqtt_config["password"],
122
+ client_id=api_inputs.data_feed_id,
123
+ email=api_inputs.email_address,
124
+ project_id=api_inputs.api_project_id)
191
125
 
192
126
  # [Connect] to MQTT Broker
193
127
  is_connected = _switch_mqtt.connect(timeout=ws_timeout_in_seconds)
@@ -348,6 +282,9 @@ def submit_control_continue(api_inputs: ApiInputs, data: pandas.DataFrame, infor
348
282
 
349
283
  _control_components = {}
350
284
 
285
+ if not control_results:
286
+ return pandas.DataFrame() # return empty DataFrame safely
287
+
351
288
  return pandas.concat(control_results, ignore_index=True)
352
289
 
353
290
 
@@ -386,7 +323,6 @@ def __send_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
386
323
  Dataframe
387
324
  control_response = is the list of sensors that were request to be controlled with status labelling if it was successful or not by the MQTTT message broker
388
325
  """
389
- global _control_ws_max_timeout
390
326
  global _switch_mqtt
391
327
  global _session
392
328
 
@@ -670,18 +606,14 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
670
606
  tuple
671
607
  control_response = is the list of sensors that were request to be controlled with status labelling if it was successful or not by the MQTTT message broker
672
608
  """
673
- global _control_api_endpoint
674
- global _control_ws_host
675
- global _control_ws_port
676
- global _control_ws_username
677
- global _control_ws_password
678
- global _control_ws_max_timeout
679
609
  global _switch_mqtt
680
610
  global _session
681
611
 
682
612
  default_control_value_column = 'DefaultControlValue'
683
613
  default_priority = 8
684
614
 
615
+ mqtt_config = _mqtt.get_control_config()
616
+
685
617
  data_frame = df.copy()
686
618
 
687
619
  if api_inputs.api_base_url == '' or api_inputs.bearer_token == '':
@@ -698,16 +630,16 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
698
630
 
699
631
  if timeout < 0:
700
632
  logger.error(
701
- f"Invalid timeout value. Timeout should be between 0 and {_control_ws_max_timeout}. Setting to zero will default to max timeout.")
633
+ f"Invalid timeout value. Timeout should be between 0 and {mqtt_config['max_timeout']}. Setting to zero will default to max timeout.")
702
634
  return 'Invalid timeout.', pandas.DataFrame()
703
635
 
704
- if timeout > _control_ws_max_timeout:
636
+ if timeout > mqtt_config["max_timeout"]:
705
637
  logger.critical(
706
638
  f'Timeout is greater than Max Timeout value. Setting timeout to Max Timeout Value instead.')
707
- timeout = _control_ws_max_timeout
639
+ timeout = mqtt_config["max_timeout"]
708
640
 
709
641
  if timeout == 0:
710
- timeout = _control_ws_max_timeout
642
+ timeout = mqtt_config["max_timeout"]
711
643
 
712
644
  if not is_valid_uuid(session_id):
713
645
  session_id = uuid.uuid4()
@@ -774,8 +706,8 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
774
706
 
775
707
  # [Create] SwitchMQTT
776
708
  if _switch_mqtt == None:
777
- _switch_mqtt = SwitchMQTT(host_address=_control_ws_host, host_port=_control_ws_port,
778
- username=_control_ws_username, password=_control_ws_password,
709
+ _switch_mqtt = SwitchMQTT(host_address=mqtt_config["ws_host"], host_port=mqtt_config["ws_port"],
710
+ username=mqtt_config["username"], password=mqtt_config["password"],
779
711
  client_id=api_inputs.data_feed_id, email=api_inputs.email_address,
780
712
  project_id=api_inputs.api_project_id)
781
713
 
@@ -892,7 +824,7 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
892
824
  control_results = process_paged_request(data_frame)
893
825
 
894
826
  return control_results
895
-
827
+
896
828
 
897
829
  def get_control_cache(api_inputs: ApiInputs, key: str, scope_id: str) -> pandas.DataFrame:
898
830
  """Gets Control Cache based on Portfolio.
@@ -1035,7 +967,8 @@ def add_control_component(name: str, column_with_object_installation_id: str, co
1035
967
 
1036
968
  def log_control_request(session: requests.Session, api_inputs: ApiInputs, df: pandas.DataFrame,
1037
969
  session_id: uuid.UUID, installation_id: uuid.UUID):
1038
-
970
+
971
+ mqtt_config = _mqtt.get_control_config()
1039
972
  json_payload = {
1040
973
  "sensors": df.to_dict(orient="records"),
1041
974
  "email": api_inputs.email_address,
@@ -1043,7 +976,7 @@ def log_control_request(session: requests.Session, api_inputs: ApiInputs, df: pa
1043
976
  "sessionId": str(session_id),
1044
977
  }
1045
978
 
1046
- url = f"{_control_api_endpoint}/api/gateway/{installation_id}/log-control-request"
979
+ url = f"{mqtt_config['api_endpoint']}/api/gateway/{installation_id}/log-control-request"
1047
980
  headers = api_inputs.api_headers.default
1048
981
 
1049
982
  logger.info("Sending Control Request to IoT API: POST %s", url)
@@ -1855,6 +1855,7 @@ def upsert_timeseries(df: pandas.DataFrame, api_inputs: ApiInputs, is_local_time
1855
1855
  df = df.assign(Carbon=0.0)
1856
1856
 
1857
1857
  df = df.copy()
1858
+ df['__row_id'] = np.arange(len(df))
1858
1859
  carbon_calcs_df = carbon_calculations_df.copy()
1859
1860
 
1860
1861
  if 'ObjectPropertyID' in carbon_calcs_df.columns:
@@ -1864,7 +1865,7 @@ def upsert_timeseries(df: pandas.DataFrame, api_inputs: ApiInputs, is_local_time
1864
1865
  if not pd.api.types.is_datetime64_any_dtype(df['TimestampLocal']):
1865
1866
  df['TimestampLocal'] = pd.to_datetime(df['TimestampLocal'])
1866
1867
 
1867
- required_carbon_cols = ['ObjectPropertyId', 'CarbonExpression', 'FromDate', 'ToDate']
1868
+ required_carbon_cols = ['ObjectPropertyId', 'CarbonExpression', 'FromDate', 'ToDate', 'ObjectPropertyTemplateID']
1868
1869
  if not all(col in carbon_calcs_df.columns for col in required_carbon_cols):
1869
1870
  logger.error(f"Missing required columns in carbon calculations DataFrame. Required: {required_carbon_cols}")
1870
1871
  return df.assign(Carbon=0.0)
@@ -1877,29 +1878,47 @@ def upsert_timeseries(df: pandas.DataFrame, api_inputs: ApiInputs, is_local_time
1877
1878
  carbon_calcs_df['ToDate'] = pd.to_datetime(carbon_calcs_df['ToDate'])
1878
1879
 
1879
1880
  merged_df = pd.merge(df, carbon_calcs_df, on='ObjectPropertyId', how='left', indicator=True)
1880
- no_valid_carbon_calcs = merged_df[merged_df._merge== 'left_only']
1881
1881
 
1882
1882
  matches_mask = (merged_df['TimestampLocal'] >= merged_df['FromDate']) & \
1883
1883
  (merged_df['TimestampLocal'] <= merged_df['ToDate'])
1884
- valid_carbon_calcs = merged_df[matches_mask]
1885
1884
 
1886
- if valid_carbon_calcs.shape[0]>0:
1887
- unique_expressions = valid_carbon_calcs['CarbonExpression'].unique()
1885
+ valid_carbon_calcs = merged_df[matches_mask].copy()
1886
+
1887
+ if not valid_carbon_calcs.empty:
1888
+ # priority: non-NULL ObjectPropertyTemplateID first
1889
+ valid_carbon_calcs["__tmplt_priority"] = valid_carbon_calcs["ObjectPropertyTemplateID"].notna().astype(int)
1890
+
1891
+ # choose ONE calc row per original df row
1892
+ valid_carbon_calcs = (
1893
+ valid_carbon_calcs
1894
+ .sort_values(["__row_id", "__tmplt_priority"], ascending=[True, False])
1895
+ .drop_duplicates(subset="__row_id", keep="first")
1896
+ )
1897
+
1898
+ unique_expressions = valid_carbon_calcs["CarbonExpression"].dropna().unique()
1888
1899
  for expr in unique_expressions:
1889
- expr_mask = valid_carbon_calcs['CarbonExpression'] == expr
1890
- numexpr_expression = expr.replace('[Value]', 'Value')
1891
- valid_carbon_calcs.loc[expr_mask, 'Carbon'] = numexpr.evaluate(
1892
- numexpr_expression,
1893
- local_dict = {'Value': valid_carbon_calcs.loc[expr_mask, 'Value'].values}
1900
+ expr_mask = valid_carbon_calcs["CarbonExpression"] == expr
1901
+ numexpr_expression = expr.replace("[Value]", "Value")
1902
+ valid_carbon_calcs.loc[expr_mask, "Carbon"] = numexpr.evaluate(
1903
+ numexpr_expression,
1904
+ local_dict={"Value": valid_carbon_calcs.loc[expr_mask, "Value"].values}
1894
1905
  )
1895
-
1896
- final_df = pd.concat([no_valid_carbon_calcs, valid_carbon_calcs])
1897
- final_row_count = final_df.shape[0]
1898
1906
 
1899
- logger.info(f"Original row count = {original_row_count}, final row count = {final_row_count}. Matches = {original_row_count==final_row_count}")
1900
- final_df = final_df.drop(columns=['CarbonExpression', 'FromDate', 'ToDate', '_merge'])
1907
+ df = df.merge(
1908
+ valid_carbon_calcs[["__row_id", "Carbon"]],
1909
+ on="__row_id",
1910
+ how="left",
1911
+ suffixes=("", "_calc"),
1912
+ )
1913
+ df["Carbon"] = df["Carbon_calc"].fillna(df["Carbon"])
1914
+ df = df.drop(columns=["Carbon_calc"])
1915
+
1916
+ final_row_count = df.shape[0]
1917
+ logger.info(
1918
+ f"Original row count = {original_row_count}, final row count = {final_row_count}. Matches = {original_row_count==final_row_count}"
1919
+ )
1901
1920
 
1902
- return final_df
1921
+ return df.drop(columns=["__row_id"])
1903
1922
 
1904
1923
  def create_meta_column(df, slice_columns, save_slices, cost_carbon_calculation):
1905
1924
  """Handles creating the 'Meta' column and optionally saves additional columns as slices."""
@@ -0,0 +1,12 @@
1
+ # -------------------------------------------------------------------------
2
+ # Copyright (c) Switch Automation Pty Ltd. All rights reserved.
3
+ # Licensed under the MIT License. See License.txt in the project root for
4
+ # license information.
5
+ # --------------------------------------------------------------------------
6
+ """
7
+ A module for sending control request of sensors.
8
+ """
9
+
10
+ from .realtime import submit_realtime_updates
11
+
12
+ __all__ = ['submit_realtime_updates']