Skip to content

Commit 758a2d0

Browse files
Extend OpenRemote Client with service registration (#42)
1 parent dd9f984 commit 758a2d0

35 files changed

+1650
-777
lines changed

docker/Dockerfile

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ARG ML_WEB_ROOT_PATH
1717
ARG ML_OR_KEYCLOAK_URL
1818
ARG ML_OR_URL
1919

20+
# Run the front-end bundle in production mode
2021
RUN ML_SERVICE_URL=${ML_SERVICE_URL:-/services/ml-forecast} \
2122
ML_WEB_ROOT_PATH=${ML_WEB_ROOT_PATH:-/services/ml-forecast/ui} \
2223
ML_OR_KEYCLOAK_URL=${ML_OR_KEYCLOAK_URL:-/auth} \
@@ -31,24 +32,27 @@ RUN echo "Starting Python build phase..."
3132
WORKDIR /app
3233

3334
ENV PYTHONUNBUFFERED=1 \
34-
PYTHONDONTWRITEBYTECODE=1 \
35-
PIP_NO_CACHE_DIR=1 \
36-
PIP_DISABLE_PIP_VERSION_CHECK=1
35+
PYTHONDONTWRITEBYTECODE=1
3736

3837
RUN echo "Installing Python build dependencies..."
3938
RUN apt-get update \
4039
&& apt-get install -y --no-install-recommends \
4140
build-essential \
41+
curl \
4242
&& rm -rf /var/lib/apt/lists/*
4343

44-
# Copy project files
45-
COPY pyproject.toml README.md ./
44+
# Install uv
45+
RUN pip install uv
46+
47+
# Copy the necessary project files
48+
COPY pyproject.toml uv.lock README.md ./
4649
COPY src/ ./src/
4750
COPY scripts/ ./scripts/
51+
COPY packages/ ./packages/
4852

49-
# Install project dependencies and clean up
53+
# Install project dependencies using uv
5054
RUN echo "Installing Python project dependencies..."
51-
RUN pip install --no-cache-dir . \
55+
RUN uv sync --no-cache-dir \
5256
&& apt-get remove -y build-essential \
5357
&& apt-get autoremove -y \
5458
&& rm -rf /var/lib/apt/lists/* \
@@ -67,27 +71,29 @@ ARG ML_ENVIRONMENT
6771
ENV PYTHONUNBUFFERED=1 \
6872
PYTHONDONTWRITEBYTECODE=1 \
6973
PYTHONPATH=/app/src \
70-
# Use the ARG, falling back to a default if not provided during build or runtime
7174
ML_ENVIRONMENT=${ML_ENVIRONMENT:-production}
7275

73-
# Install runtime dependencies and clean up
76+
# Install any runtime dependencies
7477
RUN echo "Installing runtime dependencies..."
7578
RUN apt-get update \
7679
&& apt-get install -y --no-install-recommends \
7780
curl \
7881
&& rm -rf /var/lib/apt/lists/*
7982

80-
# Copy installed packages from builder
81-
RUN echo "Copying Python packages from builder..."
82-
COPY --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/
83-
COPY --from=builder /usr/local/bin/ /usr/local/bin/
83+
# Install uv package manager
84+
RUN pip install uv
8485

8586
# Copy application code
86-
COPY pyproject.toml ./
87+
COPY pyproject.toml uv.lock README.md ./
8788
COPY src/ ./src/
8889
COPY scripts/ ./scripts/
90+
COPY packages/ ./packages/
91+
92+
# Install project dependencies using uv
93+
RUN echo "Installing Python project dependencies..."
94+
RUN uv sync --no-cache-dir
8995

90-
# Copy frontend build and clean up
96+
# Copy frontend build artifacts
9197
RUN echo "Copying frontend build artifacts..."
9298
COPY --from=frontend-builder /app/frontend/dist/ ./deployment/web/dist/
9399
RUN rm -rf /app/frontend
@@ -100,9 +106,9 @@ RUN mkdir -p ./deployment/data/models ./deployment/data/configs
100106
EXPOSE 8000
101107

102108
# Add health check
103-
HEALTHCHECK --interval=5s --timeout=5s --start-period=30s --retries=3 CMD curl --fail --silent http://localhost:8000/ui || exit 1
109+
HEALTHCHECK --interval=10s --timeout=10s --start-period=30s --retries=3 CMD curl --fail --silent http://localhost:8000/ui || exit 1
104110

105111
RUN echo "Container setup complete! Starting application..."
106112

107-
# Run the application
108-
CMD ["python", "-m", "service_ml_forecast.main"]
113+
# Run the application using uv run to ensure virtual environment is activated
114+
CMD ["uv", "run", "python", "-m", "service_ml_forecast.main"]

frontend/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626

2727
/* Outlet */
2828
#outlet {
29-
padding: 20px;
3029
min-width: fit-content;
3130
}
3231
</style>

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"serve": "cross-env rspack serve",
8+
"build:dev": "cross-env ML_OR_KEYCLOAK_URL=${ML_OR_KEYCLOAK_URL:-http://localhost:8081/auth} ML_OR_URL=${ML_OR_URL:-http://localhost:8080} ML_SERVICE_URL=${ML_SERVICE_URL:-/services/ml-forecast} ML_WEB_ROOT_PATH=${ML_WEB_ROOT_PATH:-/services/ml-forecast/ui} rspack build --mode development",
89
"build:prod": "cross-env ML_OR_KEYCLOAK_URL=${ML_OR_KEYCLOAK_URL:-/auth} ML_OR_URL=${ML_OR_URL} ML_SERVICE_URL=${ML_SERVICE_URL:-/services/ml-forecast} ML_WEB_ROOT_PATH=${ML_WEB_ROOT_PATH:-/services/ml-forecast/ui} rspack build --mode production",
910
"build:analyze": "rspack build --mode production --analyze",
1011
"lint": "eslint && prettier . --check",

frontend/rspack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export default {
7575
port: 8001,
7676
historyApiFallback: true,
7777
hot: true,
78-
watchFiles: ['/**/*'],
78+
watchFiles: ['src/**/*', 'assets/**/*', 'index.html'],
7979
headers: {
8080
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
8181
Pragma: 'no-cache',

frontend/src/pages/app-layout.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717

1818
import { createContext, provide } from '@lit/context';
1919
import { PreventAndRedirectCommands, RouterLocation } from '@vaadin/router';
20-
import { html, LitElement } from 'lit';
20+
import { css, html, LitElement, unsafeCSS } from 'lit';
2121
import { customElement, state } from 'lit/decorators.js';
2222
import { setRealmTheme } from '../common/theme';
2323
import { manager } from '@openremote/core';
24-
import { ML_OR_URL } from '../common/constants';
24+
import { IS_EMBEDDED, ML_OR_URL } from '../common/constants';
2525

2626
export const realmContext = createContext<string>(Symbol('realm'));
2727

@@ -32,6 +32,17 @@ export class AppLayout extends LitElement {
3232
@state()
3333
realm = '';
3434

35+
static get styles() {
36+
const padding = IS_EMBEDDED ? '0 20px' : '20px';
37+
38+
return css`
39+
:host {
40+
display: block;
41+
padding: ${unsafeCSS(padding)};
42+
}
43+
`;
44+
}
45+
3546
// Vaadin router lifecycle hook -- runs exactly once since this is the parent route
3647
async onBeforeEnter(location: RouterLocation, commands: PreventAndRedirectCommands) {
3748
const authRealm = manager.getRealm() ?? 'master';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[build-system]
2+
requires = ["setuptools>=42", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "openremote-client"
7+
version = "0.1.0"
8+
description = "A Python client for interacting with the OpenRemote API"
9+
readme = "README.md"
10+
requires-python = ">=3.13"
11+
12+
dependencies = ["httpx>=0.24.0", "pydantic>=2.0.0", "apscheduler>=3.11.0"]
13+
14+
[tool.setuptools]
15+
package-dir = { "" = "src" }
16+
[tool.setuptools.package-data]
17+
"openremote_client" = ["py.typed"] # include type stubs
18+
19+
[tool.setuptools.packages.find]
20+
where = ["src"]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""OpenRemote Client Package."""
2+
3+
from openremote_client.client_roles import ClientRoles
4+
from openremote_client.models import (
5+
AssetDatapoint,
6+
AssetDatapointPeriod,
7+
AssetDatapointQuery,
8+
BasicAsset,
9+
BasicAttribute,
10+
Realm,
11+
ServiceInfo,
12+
ServiceStatus,
13+
)
14+
from openremote_client.rest_client import OpenRemoteClient
15+
from openremote_client.service_registrar import OpenRemoteServiceRegistrar
16+
17+
__all__ = [
18+
"AssetDatapoint",
19+
"AssetDatapointPeriod",
20+
"AssetDatapointQuery",
21+
"BasicAsset",
22+
"BasicAttribute",
23+
"ClientRoles",
24+
"OpenRemoteClient",
25+
"OpenRemoteServiceRegistrar",
26+
"Realm",
27+
"ServiceInfo",
28+
"ServiceStatus",
29+
]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2025, OpenRemote Inc.
2+
#
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU Affero General Public License as
5+
# published by the Free Software Foundation, either version 3 of the
6+
# License, or (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU Affero General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU Affero General Public License
14+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
15+
#
16+
# SPDX-License-Identifier: AGPL-3.0-or-later
17+
18+
"""
19+
This module contains the client roles for the OpenRemote API.
20+
"""
21+
22+
23+
class ClientRoles:
24+
READ_SERVICES_ROLE = "read:services"
25+
WRITE_SERVICES_ROLE = "write:services"

src/service_ml_forecast/clients/openremote/models.py renamed to packages/openremote_client/src/openremote_client/models.py

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

18+
from enum import Enum
1819
from typing import Any
1920

2021
from pydantic import BaseModel
@@ -81,3 +82,47 @@ class Realm(BaseModel):
8182

8283
name: str
8384
displayName: str
85+
86+
87+
class ServiceStatus(str, Enum):
88+
"""The status of a registered service.
89+
90+
- AVAILABLE: The service is available and can be used
91+
- UNAVAILABLE: The service is unavailable
92+
"""
93+
94+
AVAILABLE = "AVAILABLE"
95+
UNAVAILABLE = "UNAVAILABLE"
96+
97+
98+
class ServiceInfo(BaseModel):
99+
"""Holds comprehensive details about a service.
100+
101+
This object is used to register and deregister services.
102+
"""
103+
104+
serviceId: str
105+
"""The unique identifier of the service, e.g. 'energy-service'"""
106+
107+
instanceId: int | None = None
108+
"""The unique instance identifier of the registered service,
109+
either generated by the service or provided by the user."""
110+
111+
label: str
112+
"""The label of the service, e.g. 'Energy Service'"""
113+
114+
icon: str | None = None
115+
"""The icon of the service, e.g. 'puzzle', must be part of the Material Design Icons set"""
116+
117+
version: str | None = None
118+
"""The version of the service, e.g. '1.0.0'"""
119+
120+
homepageUrl: str
121+
"""The URL of the service's homepage which provides the user interface,
122+
e.g. 'https://openremote.app/services/energy-service/ui'"""
123+
124+
status: ServiceStatus
125+
"""The status of the service, e.g. 'AVAILABLE'"""
126+
127+
realm: str
128+
"""The realm of the service, e.g. 'master'"""

0 commit comments

Comments
 (0)