Skip to content

Commit 0f9f434

Browse files
Enhanced OpenRemoteClient to allow specifying the default realm rather than defaulting to the hardcoded "master"
1 parent 945b258 commit 0f9f434

File tree

6 files changed

+55
-34
lines changed

6 files changed

+55
-34
lines changed

src/service_ml_forecast/clients/openremote/models.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,5 @@ class AssetDatapointQuery(BaseModel):
7979
class Realm(BaseModel):
8080
"""Realm model."""
8181

82-
id: str
8382
name: str
8483
displayName: str
85-
enabled: bool

src/service_ml_forecast/clients/openremote/openremote_client.py

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@
3131
Realm,
3232
)
3333

34-
MASTER_REALM = "master"
35-
3634

3735
class OAuthTokenResponse(BaseModel):
3836
"""Response model for OpenRemote OAuth token."""
@@ -56,6 +54,7 @@ class OpenRemoteClient:
5654
Args:
5755
openremote_url: The URL of the OpenRemote API.
5856
keycloak_url: The URL of the Keycloak API.
57+
realm: The default realm to use for the OpenRemote API.
5958
service_user: The service user for the OpenRemote API.
6059
service_user_secret: The service user secret for the OpenRemote API.
6160
timeout: Timeout in seconds for HTTP requests. Defaults to 30 seconds.
@@ -70,12 +69,14 @@ def __init__(
7069
self,
7170
openremote_url: str,
7271
keycloak_url: str,
72+
realm: str,
7373
service_user: str,
7474
service_user_secret: str,
7575
timeout: float = 60.0,
7676
):
7777
self.openremote_url: str = openremote_url
7878
self.keycloak_url: str = keycloak_url
79+
self.realm: str = realm
7980
self.service_user: str = service_user
8081
self.service_user_secret: str = service_user_secret
8182
self.oauth_token: OAuthTokenResponse | None = None
@@ -93,7 +94,7 @@ def __authenticate(self) -> bool:
9394
return False
9495

9596
def __get_token(self) -> OAuthTokenResponse | None:
96-
url = f"{self.keycloak_url}/realms/master/protocol/openid-connect/token"
97+
url = f"{self.keycloak_url}/realms/{self.realm}/protocol/openid-connect/token"
9798

9899
data = OAuthTokenRequest(
99100
grant_type="client_credentials",
@@ -149,18 +150,22 @@ def health_check(self) -> bool:
149150
return False
150151

151152
def get_asset_datapoint_period(
152-
self, asset_id: str, attribute_name: str, realm: str = MASTER_REALM
153+
self, asset_id: str, attribute_name: str, realm: str | None = None
153154
) -> AssetDatapointPeriod | None:
154155
"""Retrieve the datapoints timestamp period of a given asset attribute.
155156
156157
Args:
157158
asset_id: The ID of the asset.
158159
attribute_name: The name of the attribute.
160+
realm: The realm to retrieve assets from defaulting to the configured realm.
159161
160162
Returns:
161163
AssetDatapointPeriod | None: The datapoints timestamp period of the asset attribute
162164
"""
163165

166+
if realm is None:
167+
realm = self.realm
168+
164169
query = f"?assetId={asset_id}&attributeName={attribute_name}"
165170
url = f"{self.openremote_url}/api/{realm}/asset/datapoint/periods{query}"
166171

@@ -181,7 +186,7 @@ def get_historical_datapoints(
181186
attribute_name: str,
182187
from_timestamp: int,
183188
to_timestamp: int,
184-
realm: str = MASTER_REALM,
189+
realm: str | None = None,
185190
) -> list[AssetDatapoint] | None:
186191
"""Retrieve the historical data points of a given asset attribute.
187192
@@ -193,11 +198,14 @@ def get_historical_datapoints(
193198
attribute_name: The name of the attribute.
194199
from_timestamp: Epoch timestamp in milliseconds.
195200
to_timestamp: Epoch timestamp in milliseconds.
196-
realm: The realm to retrieve assets from defaulting to MASTER_REALM.
201+
realm: The realm to retrieve assets from defaulting to the configured realm.
197202
Returns:
198203
list[AssetDatapoint] | None: List of historical data points or None
199204
"""
200205

206+
if realm is None:
207+
realm = self.realm
208+
201209
params = f"{asset_id}/{attribute_name}"
202210
url = f"{self.openremote_url}/api/{realm}/asset/datapoint/{params}"
203211

@@ -219,19 +227,22 @@ def get_historical_datapoints(
219227
return None
220228

221229
def write_predicted_datapoints(
222-
self, asset_id: str, attribute_name: str, datapoints: list[AssetDatapoint], realm: str = MASTER_REALM
230+
self, asset_id: str, attribute_name: str, datapoints: list[AssetDatapoint], realm: str | None = None
223231
) -> bool:
224232
"""Write the predicted data points of a given asset attribute.
225233
226234
Args:
227235
asset_id: The ID of the asset.
228236
attribute_name: The name of the attribute.
229237
datapoints: The data points to write.
230-
realm: The realm to write the data points to defaulting to MASTER_REALM.
238+
realm: The realm to write the data points to defaulting to the configured realm.
231239
Returns:
232240
bool: True if successful
233241
"""
234242

243+
if realm is None:
244+
realm = self.realm
245+
235246
params = f"{asset_id}/{attribute_name}"
236247
url = f"{self.openremote_url}/api/{realm}/asset/predicted/{params}"
237248

@@ -254,7 +265,7 @@ def get_predicted_datapoints(
254265
attribute_name: str,
255266
from_timestamp: int,
256267
to_timestamp: int,
257-
realm: str = MASTER_REALM,
268+
realm: str | None = None,
258269
) -> list[AssetDatapoint] | None:
259270
"""Retrieve the predicted data points of a given asset attribute.
260271
@@ -263,11 +274,14 @@ def get_predicted_datapoints(
263274
attribute_name: The name of the attribute.
264275
from_timestamp: Epoch timestamp in milliseconds.
265276
to_timestamp: Epoch timestamp in milliseconds.
266-
realm: The realm to retrieve assets from defaulting to MASTER_REALM.
277+
realm: The realm to retrieve assets from defaulting to the configured realm.
267278
Returns:
268279
list[AssetDatapoint] | None: List of predicted data points or None
269280
"""
270281

282+
if realm is None:
283+
realm = self.realm
284+
271285
params = f"{asset_id}/{attribute_name}"
272286
url = f"{self.openremote_url}/api/{realm}/asset/predicted/{params}"
273287

@@ -289,17 +303,21 @@ def get_predicted_datapoints(
289303
return None
290304

291305
def asset_query(
292-
self, asset_query: dict[str, Any], query_realm: str, realm: str = MASTER_REALM
306+
self, asset_query: dict[str, Any], query_realm: str, realm: str | None = None
293307
) -> list[BasicAsset] | None:
294308
"""Perform an asset query.
295309
296310
Args:
297311
asset_query: The asset query dict to send to the OpenRemote API.
298312
query_realm: The realm for the asset query.
299-
realm: The realm to retrieve assets from defaulting to MASTER_REALM.
313+
realm: The realm to retrieve assets from defaulting to the configured realm.
300314
Returns:
301315
list[Asset] | None: List of assets or None
302316
"""
317+
318+
if realm is None:
319+
realm = self.realm
320+
303321
url = f"{self.openremote_url}/api/{realm}/asset/query"
304322
request = self.__build_request("POST", url, data=asset_query)
305323
with httpx.Client(timeout=self.timeout) as client:
@@ -313,39 +331,40 @@ def asset_query(
313331
return None
314332

315333
def get_assets_by_ids(
316-
self, asset_ids: list[str], query_realm: str, realm: str = MASTER_REALM
334+
self, asset_ids: list[str], query_realm: str, realm: str | None = None
317335
) -> list[BasicAsset] | None:
318336
"""Retrieve assets by their IDs.
319337
320338
Args:
321339
asset_ids: The IDs of the assets to retrieve.
322340
query_realm: The realm for the asset query.
323-
realm: The realm to retrieve assets from defaulting to MASTER_REALM.
341+
realm: The realm to retrieve assets from defaulting to the configured realm.
324342
325343
Returns:
326344
list[Asset] | None: List of assets or None
327345
"""
328346

347+
if realm is None:
348+
realm = self.realm
349+
329350
asset_query = {"recursive": False, "realm": {"name": query_realm}, "ids": asset_ids}
330351
return self.asset_query(asset_query, query_realm, realm)
331352

332-
def get_realms(self, realm: str = MASTER_REALM) -> list[Realm] | None:
333-
"""Retrieves all realms.
334-
335-
Args:
336-
realm: The realm to retrieve realms from defaulting to MASTER_REALM.
353+
def get_accessible_realms(self) -> list[Realm] | None:
354+
"""Retrieves all accessible realms for the current authenticated user.
337355
338356
Returns:
339357
list[Realm] | None: List of realms or None
340358
"""
341359

342-
url = f"{self.openremote_url}/api/{realm}/realm"
360+
url = f"{self.openremote_url}/api/{self.realm}/realm/accessible"
343361
request = self.__build_request("GET", url)
344362

345363
with httpx.Client(timeout=self.timeout) as client:
346364
try:
347365
response = client.send(request)
348366
response.raise_for_status()
367+
self.logger.info(f"Accessible realms: {response.json()}")
349368

350369
return [Realm(**realm) for realm in response.json()]
351370

src/service_ml_forecast/config.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,11 @@ class AppEnvironment(BaseSettings):
6464
] # origins to allow
6565

6666
# OpenRemote Settings
67-
ML_OR_URL: str = "http://localhost:8080" # OpenRemote URL
67+
ML_OR_URL: str = "http://localhost:8080" # OpenRemote Manager URL
6868
ML_OR_KEYCLOAK_URL: str = "http://localhost:8081/auth" # OpenRemote Keycloak URL
69-
ML_OR_SERVICE_USER: str = "serviceuser" # OpenRemote service user
70-
ML_OR_SERVICE_USER_SECRET: str = "secret" # OpenRemote service user secret
69+
ML_OR_REALM: str = "master" # OpenRemote realm to use for the OpenRemote Manager API
70+
ML_OR_SERVICE_USER: str = "serviceuser" # OpenRemote Manager service user
71+
ML_OR_SERVICE_USER_SECRET: str = "secret" # OpenRemote Manager service user secret
7172

7273
model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_file_encoding="utf-8", extra="ignore")
7374

src/service_ml_forecast/dependencies.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
# SPDX-License-Identifier: AGPL-3.0-or-later
1717

1818
"""
19-
This module contains the dependency injectors for the service.
20-
21-
The injectors are used to inject the services into the FastAPI app, or other dependencies.
19+
This module contains the dependency injectors and constants for the service.
2220
"""
2321

2422
import logging
@@ -35,6 +33,7 @@
3533
__openremote_client = OpenRemoteClient(
3634
openremote_url=ENV.ML_OR_URL,
3735
keycloak_url=ENV.ML_OR_KEYCLOAK_URL,
36+
realm=ENV.ML_OR_REALM,
3837
service_user=ENV.ML_OR_SERVICE_USER,
3938
service_user_secret=ENV.ML_OR_SERVICE_USER_SECRET,
4039
)
@@ -66,7 +65,7 @@ def get_openremote_issuers() -> list[str] | None:
6665
"""
6766
try:
6867
openremote_service = get_openremote_service()
69-
realms = openremote_service.get_realms()
68+
realms = openremote_service.get_accessible_realms()
7069

7170
if realms is None:
7271
return None
@@ -82,13 +81,14 @@ def get_openremote_issuers() -> list[str] | None:
8281

8382
# --- Constants ---
8483
OPENREMOTE_KC_RESOURCE = "openremote"
84+
OPENREMOTE_CLIENT_ID = "openremote"
8585

8686
# --- OAuth2 Scheme ---
8787
# This is used to allow authorization via the Docs and Redoc pages
8888
# Does not validate the token, this should be done via a middleware or manually
8989
OAUTH2_SCHEME = OAuth2PasswordBearer(
90-
tokenUrl=f"{ENV.ML_OR_KEYCLOAK_URL}/realms/master/protocol/openid-connect/token",
90+
tokenUrl=f"{ENV.ML_OR_KEYCLOAK_URL}/realms/{ENV.ML_OR_REALM}/protocol/openid-connect/token",
9191
scopes={"openid": "OpenID Connect", "profile": "User profile", "email": "User email"},
92-
description="Login into the OpenRemote Management -- Expected Client ID: 'openremote'",
92+
description=f"Login into the OpenRemote {ENV.ML_OR_REALM}'",
9393
auto_error=False,
9494
)

src/service_ml_forecast/services/openremote_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,10 @@ def get_assets_by_ids(self, realm: str, asset_ids: list[str]) -> list[BasicAsset
236236

237237
return assets
238238

239-
def get_realms(self) -> list[Realm] | None:
240-
"""Get all realms from OpenRemote.
239+
def get_accessible_realms(self) -> list[Realm] | None:
240+
"""Get all accessible realms from OpenRemote for the current authenticated user.
241241
242242
Returns:
243243
A list of all realms from OpenRemote.
244244
"""
245-
return self.client.get_realms()
245+
return self.client.get_accessible_realms()

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
# Mock URLs and credentials
3636
MOCK_OPENREMOTE_URL = "https://openremote.local"
3737
MOCK_KEYCLOAK_URL = "https://keycloak.local/auth"
38+
MOCK_KEYCLOAK_REALM = "master"
3839
MOCK_SERVICE_USER = "service_user"
3940
MOCK_SERVICE_USER_SECRET = "service_user_secret"
4041
MOCK_ACCESS_TOKEN = "mock_access_token"
@@ -69,6 +70,7 @@ def openremote_client() -> OpenRemoteClient | None:
6970
client = OpenRemoteClient(
7071
openremote_url=ENV.ML_OR_URL,
7172
keycloak_url=ENV.ML_OR_KEYCLOAK_URL,
73+
realm=ENV.ML_OR_REALM,
7274
service_user=ENV.ML_OR_SERVICE_USER,
7375
service_user_secret=ENV.ML_OR_SERVICE_USER_SECRET,
7476
)
@@ -100,6 +102,7 @@ def mock_openremote_client() -> OpenRemoteClient | None:
100102
client = OpenRemoteClient(
101103
openremote_url=MOCK_OPENREMOTE_URL,
102104
keycloak_url=MOCK_KEYCLOAK_URL,
105+
realm=MOCK_KEYCLOAK_REALM,
103106
service_user=MOCK_SERVICE_USER,
104107
service_user_secret=MOCK_SERVICE_USER_SECRET,
105108
)

0 commit comments

Comments
 (0)