File: __init__.py

package info (click to toggle)
python-aiooncue 0.3.7-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 204 kB
  • sloc: python: 592; makefile: 79; sh: 2
file content (271 lines) | stat: -rw-r--r-- 8,903 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
"""Top-level package for Async Oncue."""
from __future__ import annotations

from dataclasses import dataclass
import json

import aiohttp

__author__ = """J. Nick Koston"""
__email__ = "nick@koston.org"
__version__ = "0.3.7"

from .const import NAME_TO_SENSOR_ID

REQUIRED_DEVICE_KEYS = {
    "displayname",
    "devicestate",
    "productname",
    "version",
    "serialnumber",
}
REQUIRED_DEVICE_SENSOR_KEYS = {"FirmwareVersion", "GensetModelNumberSelect"}

DEFAULT_REQUEST_TIMEOUT = 15

BASE_ENDPOINT = "https://api.kohler.com/krm/v1"

COMMAND_ENDPOINT = "/devices/command"

LIST_DEVICES_ENDPOINT = "/devices/listdevices"
LIST_DEVICES_PARAMETERS = "[4,11,60,69,102,91,114,115,549]"

DEVICE_DETAILS_ENDPOINT = "/devices/devicedetails"
DEVICE_DETAILS_PARAMETERS = (
    "[2,3,4,6,7,11,18,20,32,55,56,60,69,93,102,113,114,115,864,870,872,1671]"
)

ALL_DETAILS_NAMES = [
    "Controller Type",  # 2
    "Current Firmware",  # 3
    "Engine Speed",  # 4
    "Engine Target Speed",  # 5
    "Engine Oil Pressure",  # 6
    "Engine Coolant Temperature",  # 7
    "Battery Voltage",  # 11
    "Lube Oil Temperature",  # 18
    "Generator Controller Temperature",  # 20
    "Engine Compartment Temperature",  # 32
    "Generator True Total Power",  # 55
    "Generator True Percent Of Rated Power",  # 56
    "Generator Voltage AB",  # 57
    "Generator Voltage Average Line To Line",  # 60
    "Generator Current Average",  # 68
    "Generator Frequency",  # 69
    "Genset Model Number Select",  # 91
    "Generator Serial Number",  # 93
    "Generator Controller Serial Number",  # 95
    "Generator State",  # 102
    "Generator Controller Clock Time",  # 113,
    "Generator Controller Total Operation Time",  # 114,
    "Engine Total Run Time",  # 115,
    "Engine Total Run Time Loaded",  # 116,
    "Engine Total Number Of Starts",  # 118,
    "Genset Total Energy",  # 119   -- note that this can be 1.1986518E7
    "Ats Contactor Position",  # 549
    "Ats Sources Available",  # 550,
    "Source1 Voltage Average Line To Line",  # 588,
    "Source2 Voltage Average Line To Line",  # 623,
    "IP Address",  # 864,
    "Mac Address",  # 869
    "Connected Server IP Address",  # 870
    "Network Connection Established",  # 872
    "Serial Number",  # 908
    "Latest Firmware",  # 1671
]

ALL_DEVICES_PARAMETERS = json.dumps(
    [[NAME_TO_SENSOR_ID[name] for name in ALL_DETAILS_NAMES]], separators=(",", ":")
)

LOGIN_SESSION_EXPIRED = 1200
LOGIN_INVALID_USERNAME = 1202
LOGIN_INVALID_PASSWORD = 1207

LOGIN_FAILED_CODES = {
    0: "Unknown",
    LOGIN_SESSION_EXPIRED: "Session Expired",
    LOGIN_INVALID_USERNAME: "Invalid username",
    LOGIN_INVALID_PASSWORD: "Invalid Password",
}
INCORRECT_CREDENTIALS_CODES = {LOGIN_INVALID_PASSWORD}

LOGIN_ENDPOINT = "/users/connect"


@dataclass
class OncueSensor:
    name: str
    display_name: str
    value: str
    display_value: str
    unit: str | None


@dataclass
class OncueDevice:
    name: str
    state: str
    product_name: str
    hardware_version: str
    serial_number: str
    sensors: dict[str, OncueSensor]


class OncueException(Exception):
    """Base oncue exception."""


class LoginFailedException(OncueException):
    """Login failed exception."""


class ServiceFailedException(OncueException):
    """Service failed exception."""


class Oncue:
    """Async oncue api."""

    def __init__(
        self,
        username: str,
        password: str,
        websession: aiohttp.ClientSession,
        timeout: int = DEFAULT_REQUEST_TIMEOUT,
    ):
        """Create oncue async api object."""
        self._websession = websession
        self._timeout = timeout
        self._username = username
        self._password = password
        self._auth_invalid = 0

    async def _get(self, endpoint: str, params=None) -> dict:
        """Make a get request."""
        response = await self._websession.request(
            "GET",
            f"{BASE_ENDPOINT}{endpoint}",
            timeout=self._timeout,
            params=params,
        )
        return await response.json()

    async def _get_authenticated(self, endpoint: str, params=None) -> dict:
        if self._auth_invalid:
            raise LoginFailedException(
                f"Authorization invalid will not retry - {self._auth_invalid}"
            )
        for _ in range(2):
            data = await self._get(endpoint, {"sessionkey": self._sessionkey, **params})
            if "code" not in data or data["code"] not in LOGIN_FAILED_CODES:
                return data
            await self.async_login()

        raise LoginFailedException(self._auth_invalid)

    async def async_login(self) -> None:
        """Call api to login"""
        login_data = await self._get(
            LOGIN_ENDPOINT, {"username": self._username, "password": self._password}
        )

        if "code" in login_data:
            code = login_data["code"]
            message = login_data.get("message", "no message")
            if code in INCORRECT_CREDENTIALS_CODES:
                self._auth_invalid = f"{message} ({code})"
                raise LoginFailedException(self._auth_invalid)
            raise ServiceFailedException(
                f"Login failed with unknown error code: {code} ({message})"
            )

        self._sessionkey = login_data["sessionkey"]

    async def async_fetch_all(self) -> dict[str, OncueDevice]:
        """Fetch all devices."""
        devices = await self.async_list_devices_with_params()
        indexed_devices: dict[str, OncueDevice] = {}
        for device in devices:
            if REQUIRED_DEVICE_KEYS.intersection(device) != REQUIRED_DEVICE_KEYS:
                continue
            sensors: dict[str, OncueSensor] = {}
            for sensor in device["parameters"]:
                value = sensor["value"]
                name = sensor["name"]
                unit = None
                if len(sensor["displayvalue"]) > len(str(value)) + 1:
                    unit = sensor["displayvalue"][len(str(value)) + 1 :]
                sensors[name] = OncueSensor(
                    name=name,
                    display_name=sensor["displayname"],
                    value=sensor["value"],
                    display_value=sensor["displayvalue"],
                    unit=unit,
                )
            if (
                REQUIRED_DEVICE_SENSOR_KEYS.intersection(sensors)
                != REQUIRED_DEVICE_SENSOR_KEYS
            ):
                continue
            indexed_devices[device["id"]] = OncueDevice(
                name=device["displayname"],
                state=device["devicestate"],
                product_name=device["productname"],
                hardware_version=device["version"],
                serial_number=device["serialnumber"],
                sensors=sensors,
            )
        return indexed_devices

    async def async_list_devices_with_params(self):
        """Call api to list devices."""
        return await self._get_authenticated(
            LIST_DEVICES_ENDPOINT,
            {
                "events": "active",
                "showperipheraldetails": "true",
                "parameters": ALL_DEVICES_PARAMETERS,
            },
        )

    async def async_list_devices(self):
        """Call api to list devices"""
        return await self._get_authenticated(
            LIST_DEVICES_ENDPOINT,
            {
                "events": "active",
                "showperipheraldetails": "true",
                "parameters": LIST_DEVICES_PARAMETERS,
            },
        )

    async def async_device_details(self, device: str) -> dict:
        """Call api to get device devices"""
        return await self._get_authenticated(
            LIST_DEVICES_ENDPOINT,
            {"device": device, "parameters": DEVICE_DETAILS_PARAMETERS},
        )

    async def async_start_loaded_full_speed_exercise(self, device: str) -> None:
        """Call api to stat a loaded full speed exercise."""
        await self._async_do_action(device, "startloadedfullspeedexercise")

    async def async_start_unloaded_full_speed_exercise(self, device: str) -> None:
        """Call api to stat an unloaded full speed exercise."""
        await self._async_do_action(device, "startunloadedfullspeedexercise")

    async def async_start_unloaded_cycle_exercise(self, device: str) -> None:
        """Call api to stat an unloaded cycle exercise."""
        await self._async_do_action(device, "startunloadedcycleexercise")

    async def async_end_exercise(self, device: str) -> None:
        """Call api to end an exercise."""
        await self._async_do_action(device, "endexercise")

    async def _async_do_action(self, device: str, action: str) -> None:
        """Call api to do an action."""
        await self._get_authenticated(
            LIST_DEVICES_ENDPOINT,
            {"device": device, "service": "doaction", "value": action},
        )