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.
- {switch_api-0.6.12 → switch_api-0.6.14}/HISTORY.md +16 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/PKG-INFO +17 -1
- {switch_api-0.6.12 → switch_api-0.6.14}/setup.py +1 -1
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/__init__.py +4 -2
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_utils/_constants.py +6 -0
- switch_api-0.6.14/switch_api/_utils/_mqtt.py +124 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/controls/__init__.py +2 -3
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/controls/_constants.py +0 -6
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/controls/_mqtt.py +2 -3
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/controls/controls.py +49 -116
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/integration/integration.py +35 -16
- switch_api-0.6.14/switch_api/realtime/__init__.py +12 -0
- switch_api-0.6.14/switch_api/realtime/_mqtt.py +205 -0
- switch_api-0.6.14/switch_api/realtime/realtime.py +235 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api.egg-info/PKG-INFO +17 -1
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api.egg-info/SOURCES.txt +5 -1
- {switch_api-0.6.12 → switch_api-0.6.14}/AUTHORS.rst +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/LICENCE +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/MANIFEST.in +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/README.md +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/pyproject.toml +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/setup.cfg +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/_authentication.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/_credentials_store/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/_credentials_store/_credentials_store.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/_msal/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_authentication/_msal/_custom_application.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/_commands.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/_compute.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/_iotservice.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/_models.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_compute/_utils.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_guide/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_guide/main.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_guide/processor.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_utils/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_utils/_marketplace.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_utils/_platform.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/_utils/_utils.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/analytics/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/analytics/analytics.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/cache/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/cache/cache.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/controls/_enums.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/dataset/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/dataset/dataset.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/email/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/email/email_sender.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/error_handlers/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/error_handlers/error_handlers.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/extensions/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/extensions/extensions.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/extensions/field_meta.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/extensions/helpers.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/extensions/pipeline.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/initialize.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/integration/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/integration/_utils.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/integration/helpers.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/pipeline/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/pipeline/automation.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/pipeline/definitions.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/pipeline/pipeline.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/platform_insights/__init__.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api/platform_insights/platform_insights.py +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api.egg-info/dependency_links.txt +0 -0
- {switch_api-0.6.12 → switch_api-0.6.14}/switch_api.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
10
|
+
__all__ = ['submit_control', 'submit_control_continue', 'add_control_component']
|
|
11
11
|
|
|
12
|
-
|
|
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
|
|
20
|
-
|
|
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
|
|
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,
|
|
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 {
|
|
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 >
|
|
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 =
|
|
111
|
+
ws_timeout_in_seconds = mqtt_config["max_timeout"]
|
|
178
112
|
|
|
179
113
|
if ws_timeout_in_seconds == 0:
|
|
180
|
-
ws_timeout_in_seconds =
|
|
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=
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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 {
|
|
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 >
|
|
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 =
|
|
639
|
+
timeout = mqtt_config["max_timeout"]
|
|
708
640
|
|
|
709
641
|
if timeout == 0:
|
|
710
|
-
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=
|
|
778
|
-
username=
|
|
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"{
|
|
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
|
-
|
|
1887
|
-
|
|
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[
|
|
1890
|
-
numexpr_expression = expr.replace(
|
|
1891
|
-
valid_carbon_calcs.loc[expr_mask,
|
|
1892
|
-
numexpr_expression,
|
|
1893
|
-
local_dict
|
|
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
|
-
|
|
1900
|
-
|
|
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
|
|
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']
|