Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
185 commits
Select commit Hold shift + click to select a range
87fef87
poc of websockets using flask-sock
Mar 6, 2025
902c8cd
apply pre-commit
Mar 6, 2025
b54c530
poc: test if we can get the app context
Mar 18, 2025
c1abc0b
use app conect to release/attatch the connection to/from the connecti…
Mar 25, 2025
05ecea1
Test: Headers exchange between server and client side
VladIftime Mar 25, 2025
37450d5
Merge branch 'exp/websockets/flask-sock-poc' of https://github.com/Fl…
VladIftime Mar 25, 2025
ce51a17
add analysis notebook
Mar 26, 2025
66937a2
Merge remote-tracking branch 'origin' into exp/websockets/flask-sock-poc
Mar 26, 2025
a056fb4
Merge remote-tracking branch 'origin/exp/websockets/flask-sock-poc' i…
Mar 26, 2025
d11f00e
test change
Ahmad-Wahid Jun 24, 2025
8449eaa
Trying to use S2 messages
VladIftime Jun 25, 2025
b3fe923
Merge remote-tracking branch 'refs/remotes/origin/main' into exp/webs…
Flix6x Jun 25, 2025
40c09bb
Merge remote-tracking branch 'refs/remotes/origin/exp/websockets/flas…
Flix6x Jun 25, 2025
fb9db22
style: flake8
Flix6x Jun 13, 2025
a6a4c1d
style: black
Flix6x Jun 25, 2025
fe9fff4
dev: add s2-python requirement
Flix6x Jun 26, 2025
a9f93ce
run ws in async mode
Ahmad-Wahid Jun 30, 2025
ae838dc
run ws in async mode
Ahmad-Wahid Jun 30, 2025
e4bbd7a
Trying to run async ws
VladIftime Jun 30, 2025
0ea5500
Merged
VladIftime Jun 30, 2025
8348f5a
Syncronus ws server handling works
VladIftime Jul 1, 2025
ad9fec2
Renamed the rm s2 example to make it a test
VladIftime Jul 8, 2025
1dbeab3
refactor: move test file to tests module within ws subpackage
Flix6x Jul 9, 2025
f06b11e
refactor: set SERVER_URL in one place
Flix6x Jul 9, 2025
18a02c8
feat: add auth required to WS endpoint
Flix6x Aug 13, 2025
9cf4e90
fix: auth check
Flix6x Aug 13, 2025
b6b37f6
fix: less noisy 401, by moving the auth check to before upgrading the…
Flix6x Aug 13, 2025
21dd9de
feat: support bearer token set up in config settings
Flix6x Aug 13, 2025
051a00f
fix: register handle_ResourceManagerDetails
Flix6x Aug 13, 2025
a36d749
dev: add scaffolding with todos
Flix6x Aug 13, 2025
7c65b84
Full pipeline fails at db
VladIftime Aug 25, 2025
101fdb4
Full pipeline: can now call the sceduler without errors on the server…
VladIftime Aug 26, 2025
208ea6a
Full pipeline: can now call the sceduler without errors on the server…
VladIftime Aug 26, 2025
76469b1
Full pipeline: fixed alignement issue
VladIftime Aug 26, 2025
7552157
Full pipline: FRBCInstrucions are generated and sent to the client. S…
VladIftime Aug 26, 2025
04dcaf8
Full pipline: FRBCInstrucions are generated correctly but the schedul…
VladIftime Sep 1, 2025
07fd98e
Removed unnecessary s2python versioning
VladIftime Sep 3, 2025
4ef8b27
Removed unnecessary async version of ws message handling
VladIftime Sep 3, 2025
4e554b4
fix: do not crash in `finally` if s2_scheduler has no remove_device_s…
Flix6x Sep 4, 2025
5bb90ff
dev: add debug statements explaining why the scheduling is not trigge…
Flix6x Sep 12, 2025
ff6f42f
fix: plural in method name
Flix6x Sep 12, 2025
3538acf
fix: fix method name
Flix6x Sep 12, 2025
afffdd9
dev: comment out a call
Flix6x Sep 12, 2025
d9c078b
feat: visual aides
Flix6x Sep 12, 2025
0a50d5e
style: less obtrusive visual aides
Flix6x Sep 12, 2025
e5cc197
Revert "style: less obtrusive visual aides"
Flix6x Sep 12, 2025
14ea5fa
style: align visual aides
Flix6x Sep 12, 2025
001a16f
dev: try setting s2_scheduler.device_data
Flix6x Sep 12, 2025
4c79bac
style: actual alignment
Flix6x Sep 12, 2025
4786dec
fix: set s2_scheduler.frbc_device_data instead
Flix6x Sep 12, 2025
d20d683
fix: mypy
Flix6x Sep 12, 2025
8fa1069
refactor: make debugging statements DRY
Flix6x Sep 12, 2025
3d42999
fix: HandshakeResponse should set new unique message ID, rather than …
Flix6x Sep 12, 2025
c130755
fix: select client-supported protocol version that is also supported …
Flix6x Sep 12, 2025
d0d3099
dev: log tracebacks in debug level only
Flix6x Sep 12, 2025
db259e0
dev: log sent messages, too
Flix6x Sep 12, 2025
417df9d
feat: better separate info and debug log statement
Flix6x Sep 17, 2025
3ace626
Added state tracking for scheduling calling
VladIftime Sep 22, 2025
5778dab
Clean-up of instruction list before sending it
VladIftime Sep 23, 2025
b57eca4
fix: Update WebSocket integration to use S2FlaskScheduler
VladIftime Sep 23, 2025
bc3aa90
docs: add more context to info log message
Flix6x Sep 24, 2025
fb0fb61
style: black and flake8
Flix6x Sep 24, 2025
53b7e03
dev: don't (debug) log S2 messages that are too verbose
Flix6x Sep 24, 2025
04f2eaf
fix: typo
Flix6x Sep 24, 2025
0d12860
Wait for all actuator statuses to be sent before scheduling
VladIftime Sep 24, 2025
d0ec879
Chore: cleaned logs from Reception status
VladIftime Sep 24, 2025
870b3d5
refactor: ensure_resource_is_registered
Flix6x Sep 24, 2025
a11f999
style: black
Flix6x Sep 24, 2025
0a4384d
fix: unresolved attribute reference (typing warning)
Flix6x Sep 29, 2025
2392bf1
refactor: simplify condition
Flix6x Sep 29, 2025
1de460c
feat: switch from to WEBSOCKET_BEARER_TOKEN to FLEXMEASURES_S2_BEARER…
Flix6x Sep 29, 2025
0055462
feat: create unique asset for each unique resource
Flix6x Sep 29, 2025
d73a689
fix: initialize properties with empty defaults
Flix6x Sep 29, 2025
ecd6f20
dev: debug using server log
Flix6x Oct 1, 2025
18582eb
dev: add extra security
Flix6x Oct 1, 2025
6e7a196
dev: switch to login required
Flix6x Oct 1, 2025
2378b33
Revert "fix: initialize properties with empty defaults"
Flix6x Oct 1, 2025
858c0c5
Revert "feat: create unique asset for each unique resource"
Flix6x Oct 1, 2025
c45472d
Revert "feat: switch from to WEBSOCKET_BEARER_TOKEN to FLEXMEASURES_S…
Flix6x Oct 1, 2025
87ebebb
dev: yield occasionally
Flix6x Oct 1, 2025
bcc8404
dev: switch away from live-streaming
Flix6x Oct 1, 2025
c663c32
dev: set dynamic line limit
Flix6x Oct 1, 2025
9ee53a6
dev: try dequeueing
Flix6x Oct 1, 2025
98c839d
dev: update method name
Flix6x Oct 1, 2025
9febee1
docs: changelog entry
VladIftime Oct 9, 2025
88aebb1
Instrunction get filtered, then the previous Instructions are revoked…
VladIftime Oct 14, 2025
a77600f
feat: create unique asset for each unique resource
Flix6x Sep 29, 2025
3c48df6
dev: debug resource registration as assets
Flix6x Oct 22, 2025
c9895a6
style: black
Flix6x Oct 22, 2025
70ed473
feat: switch from to WEBSOCKET_BEARER_TOKEN to FLEXMEASURES_S2_BEARER…
Flix6x Sep 29, 2025
84fff16
fix: initialize properties with empty defaults
Flix6x Sep 29, 2025
e2d6b8b
dev: debug FLEXMEASURES_S2_BEARERS
Flix6x Oct 22, 2025
337b00f
fix: creating an Asset requires an AssetType
Flix6x Oct 22, 2025
9eecc40
feat: warn if resource could not be saved as an asset, but still cont…
Flix6x Oct 22, 2025
7e465a3
feat: finish transaction
Flix6x Oct 22, 2025
ec09b6a
fix: remove CLI specific log statement in services/utils.py
Flix6x Oct 22, 2025
19365f6
feat: register actuators
Flix6x Oct 22, 2025
9394356
feat: register actuator as child asset of resource
Flix6x Oct 22, 2025
50dbe68
fix: UUID to str
Flix6x Oct 22, 2025
85c9a62
feat: save fill level updates
Flix6x Oct 22, 2025
f41edd8
feat: catch any errors while committing and proceed to operate withou…
Flix6x Oct 22, 2025
c3649b5
feat: rate limit saving fill levels
Flix6x Oct 22, 2025
db7ad02
fix: rate limit per resource
Flix6x Oct 22, 2025
8d5d9cb
refactor: move timer logic to decorator
Flix6x Oct 22, 2025
5e99ff0
fix: create sensor
Flix6x Oct 22, 2025
2e580f5
feat: still attempt to create sensor
Flix6x Oct 22, 2025
af154fa
fix: create data source for user
Flix6x Oct 22, 2025
5d9935f
refactor: towards reusing class method
Flix6x Oct 22, 2025
e553867
feat: accept multiple kwargs for separating timers
Flix6x Oct 22, 2025
556147c
refactor: save_fill_level becomes save_event
Flix6x Oct 22, 2025
c0e59e0
feat: timers due on the tick
Flix6x Oct 22, 2025
2f28c1c
feat: events recorded on the tick
Flix6x Oct 22, 2025
5018676
fix: truthfully report the time of recording
Flix6x Oct 22, 2025
980f27a
Improved logging
VladIftime Oct 23, 2025
87c3ee9
Added InstructionStatusUpdate handler
VladIftime Oct 23, 2025
1bc31a3
Added InstructionStatusUpdate handler
VladIftime Oct 23, 2025
2b5bb0b
Fixed alignment issue and added db timeout
VladIftime Oct 23, 2025
eb05447
Merge branch 'exp/websockets/flask-sock-poc-test' of github.com:FlexM…
VladIftime Oct 23, 2025
1769e78
feat: save SystemDescription as asset attributes
Flix6x Oct 23, 2025
4a81e9f
refactor: save_event also accepts actuator ID
Flix6x Oct 23, 2025
81353fd
fix: update decorator
Flix6x Oct 23, 2025
a6c7678
fix: timezone aware reference
Flix6x Oct 23, 2025
a955651
fix: timezone aware datetime.min out of range
Flix6x Oct 23, 2025
5a7441b
refactor: variable renaming
Flix6x Oct 23, 2025
e4dc737
fix: set_attribute assumes attribute already exists
Flix6x Oct 23, 2025
0f78fd7
fix: first convert to JSON then load as dict
Flix6x Oct 23, 2025
24fcf58
feat: save device schedules
Flix6x Oct 23, 2025
b6651e6
fix: pass string device ID
Flix6x Oct 23, 2025
643afea
fix: missing default value for kwarg
Flix6x Oct 24, 2025
dcb3df2
fix: convert energy to power units using resolution
Flix6x Oct 24, 2025
65ddbdf
fix: create data source early
Flix6x Oct 24, 2025
85ea259
fix: one more unit conversion
Flix6x Oct 24, 2025
99f5361
feat: localize sensor to FLEXMEASURES_TIMEZONE
Flix6x Oct 24, 2025
61e5cbf
dev: log saving of scheduling results
Flix6x Oct 24, 2025
44ce434
feat: S2Scheduler gets distinct data source from user-sent data
Flix6x Oct 24, 2025
ad4ffde
Merge branch 'exp/websockets/flask-sock-poc-test' of github.com:FlexM…
VladIftime Oct 28, 2025
5409e67
Sending and tracking instruction status update
VladIftime Oct 28, 2025
f5414c8
Added improved logging
VladIftime Oct 28, 2025
8f5f02b
docs: fix typos
Flix6x Oct 24, 2025
7456359
feat: save scheduled fill level
Flix6x Oct 29, 2025
622d12f
fix: wrong dict key
Flix6x Oct 29, 2025
ff42721
scheduling 15 min ahead
VladIftime Oct 29, 2025
5f6d9a6
scheduling 15 min ahead
VladIftime Oct 29, 2025
a1b2943
feat: save PowerMeasurement
Flix6x Oct 30, 2025
9af1f8d
style: black
Flix6x Oct 30, 2025
3e7534c
15 minute delay for schedule and fixed revoking
VladIftime Nov 5, 2025
5131bab
feat: use TEMPORARY_ERROR instead of PERMANENT_ERROR for field tests
Flix6x Nov 5, 2025
aa08c85
style: black
Flix6x Nov 5, 2025
f7f44d9
fix: PowerMeasurement.values
Flix6x Nov 5, 2025
36137db
fix: coding error
Flix6x Nov 5, 2025
63b7bbc
fix: instruction.id rather than instruction.instruction_id
Flix6x Nov 5, 2025
522770b
Handler for leakeage and usage behaviour
VladIftime Nov 5, 2025
89cf9af
Handler for leakeage and usage behaviour
VladIftime Nov 5, 2025
43a0f6d
Merge branch 'exp/websockets/flask-sock-poc-test' of https://github.c…
VladIftime Nov 6, 2025
16e2c92
alignment for fill level profile and usage profile
VladIftime Nov 6, 2025
08d0ce6
alignment for fill level profile and usage profile
VladIftime Nov 6, 2025
c5ea21d
alignment for fill level profile and usage profile
VladIftime Nov 6, 2025
7b4c925
fix: fetch fresh data source
Flix6x Nov 7, 2025
609f027
fix: save PowerMeasurement
Flix6x Nov 7, 2025
7dfb6a9
fix: save measurement value rather than message value
Flix6x Nov 7, 2025
c9592e8
refactor: pass data source ID instead of data source
Flix6x Nov 7, 2025
ef66839
dev: log saved events
Flix6x Nov 7, 2025
2c93c50
fix: use new kwarg name
Flix6x Nov 7, 2025
2481f46
fix: save fill level events from two sources
Flix6x Nov 7, 2025
3e0c6f6
fix: warning message
Flix6x Nov 9, 2025
b9a0feb
dev: namespace the logging of S2 related contents using the RM name
Flix6x Dec 3, 2025
b679f5d
feat: pytest covering WS endpoint
Flix6x Dec 3, 2025
a4ea105
refactor: move connected WS to fixture
Flix6x Dec 3, 2025
4fdfde9
refactor: make fixture reusable for connecting to other WS endpoints
Flix6x Dec 3, 2025
3be63f4
feat: do not disable existing loggers
Flix6x Dec 5, 2025
f23fd40
feat: only re-enable s2python logger instead of re-enabling all loggers
Flix6x Dec 5, 2025
b1dcda8
dev: set s2python logger level to DEBUG
Flix6x Dec 5, 2025
ba08bcf
feat: make s2-python log statements visually distinct
Flix6x Dec 5, 2025
403936d
fix: failsafe against missing resource logger
Flix6x Dec 5, 2025
6593811
feat: add type annotations to _logger
Flix6x Dec 5, 2025
72d5e9d
fix: commit the users in the setup_roles_users fixture, so that they …
Flix6x Dec 5, 2025
7c4b1bb
dev: also log flexmeasures-s2 with a color
Flix6x Dec 5, 2025
91cd71f
dev: use pairing token as bearer token
Flix6x Dec 7, 2025
81b7b3a
refactor: move S2FlaskWSServerSync from flexmeasures to flexmeasures-…
Flix6x Dec 7, 2025
a1437a1
refactor: move ws_connection_auth from flexmeasures to flexmeasures-s2
Flix6x Dec 8, 2025
29dd6b0
refactor: move test_s2_client_rm.py from flexmeasures to flexmeasures-s2
Flix6x Dec 8, 2025
6c0d9e6
refactor: move s2-python[ws] requirement from flexmeasures to flexmea…
Flix6x Dec 8, 2025
18e8476
chore: clean up diff
Flix6x Dec 8, 2025
0a780b0
refactor: move import to top of module
Flix6x Dec 8, 2025
351c586
Merge remote-tracking branch 'refs/remotes/origin/main' into exp/webs…
Flix6x Jan 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"spellright.language": [
"en_US"
],
"spellright.language": ["en_US"],
"spellright.documentTypes": [
"markdown",
"latex",
Expand All @@ -14,10 +12,10 @@
"python.linting.flake8Enabled": true,
"workbench.editor.wrapTabs": true,
"python.formatting.provider": "black",
"python.testing.pytestArgs": [
"flexmeasures"
],
"python.testing.pytestArgs": ["flexmeasures"],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.analysis.autoImportCompletions": true
"python.analysis.autoImportCompletions": true,
"stm32-for-vscode.openOCDPath": false,
"stm32-for-vscode.armToolchainPath": false
}
24 changes: 23 additions & 1 deletion flexmeasures/api/common/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from flask_security import auth_token_required
from collections import deque
import os

from flask import current_app, request, Response
from flask_security import auth_token_required, login_required
from werkzeug.exceptions import NotFound, abort

from flexmeasures.auth.decorators import roles_required
from flexmeasures.api.common import flexmeasures_api as flexmeasures_api_ops
Expand All @@ -20,3 +25,20 @@ def get_task_run():
@roles_required("task-runner")
def post_task_run():
return ops_impl.post_task_run()


@flexmeasures_api_ops.route("/logs")
@login_required
@roles_required("debugger")
def show_logs():
"""Show server logs for debugging."""
if current_app.config.get("LOGGING_LEVEL") != "DEBUG":
raise NotFound

log_file = "flexmeasures.log"
n = int(request.args.get("tail", 200))
if not os.path.exists(log_file):
abort(404, "Log file not found")
with open(log_file, "r") as f:
last_n_lines = deque(f, maxlen=n)
return Response("".join(last_n_lines), mimetype="text/plain")
2 changes: 2 additions & 0 deletions flexmeasures/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from rq import Queue

from flexmeasures.data.services.job_cache import JobCache
from flexmeasures.ws import sock


def create( # noqa C901
Expand Down Expand Up @@ -56,6 +57,7 @@ def create( # noqa C901
cfg_location = find_flexmeasures_cfg() # Find flexmeasures.cfg location
# Create app
app = Flask("flexmeasures")
sock.init_app(app)

if env is not None: # overwrite
app.config["FLEXMEASURES_ENV"] = env
Expand Down
1 change: 1 addition & 0 deletions flexmeasures/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ def create_roles_users(db, test_accounts) -> dict[str, User]:
password="testtest",
)
)
db.session.commit()
return {user.username: user.id for user in new_users}


Expand Down
4 changes: 2 additions & 2 deletions flexmeasures/data/models/data_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ def get_data_source_info(cls: type) -> dict:
@property
def data_source(self) -> "DataSource":
"""DataSource property derived from the `source_info`: `source_type` (scheduler, forecaster or reporter), `model` (e.g AggregatorReporter)
and `attributes`. It looks for a data source in the database the marges the `source_info` and, in case of not finding any, it creates a new one.
This property gets created once and it's cached for the rest of the lifetime of the DataGenerator object.
and `attributes`. It looks for a data source in the database that matches the `source_info` and, in case of not finding any, it creates a new one.
This property gets created once, and it's cached for the rest of the lifetime of the DataGenerator object.
"""

from flexmeasures.data.services.data_sources import get_or_create_source
Expand Down
2 changes: 0 additions & 2 deletions flexmeasures/data/services/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from copy import deepcopy
import inspect

import click
from sqlalchemy import JSON, String, cast, literal
from flask import current_app
from rq import Queue
Expand Down Expand Up @@ -130,7 +129,6 @@ def get_or_create_model(
model = model_class(**init_kwargs)
db.session.add(model)
db.session.flush() # assign ID
click.echo(f"Created {repr(model)}")
return model


Expand Down
1 change: 1 addition & 0 deletions flexmeasures/ui/static/openapi-specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"/api/ops/ping": {},
"/api/ops/getLatestTaskRun": {},
"/api/ops/postLatestTaskRun": {},
"/api/ops/logs": {},
"/api/v3_0/sensors/{id}": {
"delete": {
"summary": "Delete a sensor",
Expand Down
39 changes: 39 additions & 0 deletions flexmeasures/utils/coding_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,42 @@ def __hash__(self):
f"Unhashable object: {self} has no ID. Consider calling `db.session.flush()` before using {type(self).__name__} objects in sets or as dictionary keys."
)
return hash(self.id)


def only_if_timer_due(*kwarg_names):
"""
Decorator that runs a method only if its timer is due.

Timer name is derived from:
"<function_name> for <kwarg1_value>, <kwarg2_value>, ..."

:param kwarg_names: names of kwargs to include in the timer name
"""

def decorator(func):
sig = inspect.signature(func) # get function signature

@functools.wraps(func)
def wrapper(self, *args, **kwargs):
timer_name = func.__name__ # start with the function name

if kwarg_names:
bound_args = sig.bind(self, *args, **kwargs)
bound_args.apply_defaults()

values = []
for name in kwarg_names:
if name in bound_args.arguments:
values.append(str(bound_args.arguments[name]))
else:
raise ValueError(f"Keyword '{name}' not found in function call")

timer_name += " for " + ", ".join(values)

if not self._is_timer_due(timer_name):
return # skip if timer not due
return func(self, *args, **kwargs)

return wrapper

return decorator
26 changes: 25 additions & 1 deletion flexmeasures/utils/config_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
"version": 1,
"formatters": {
"default": {"format": "[FLEXMEASURES][%(asctime)s] %(levelname)s: %(message)s"},
"s2python": {
"format": "\033[94m[FLEXMEASURES][%(asctime)s][%(name)s] %(levelname)s: %(message)s\033[0m"
}, # blue
"s2rm": {
"format": "\033[91m[FLEXMEASURES][%(asctime)s][%(name)s] %(levelname)s: %(message)s\033[0m"
},
"detail": {
"format": "[FLEXMEASURES][%(asctime)s] %(levelname)s: %(message)s [logged in %(pathname)s:%(lineno)d]"
},
Expand All @@ -37,6 +43,16 @@
"stream": sys.stdout,
"formatter": "default",
},
"s2console": { # handler specific to s2python
"class": "logging.StreamHandler",
"stream": sys.stdout,
"formatter": "s2python",
},
"s2rmconsole": { # handler specific to s2python-rm
"class": "logging.StreamHandler",
"stream": sys.stdout,
"formatter": "s2rm",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "INFO",
Expand All @@ -46,7 +62,15 @@
"backupCount": 6,
},
},
"root": {"level": "INFO", "handlers": ["console", "file"], "propagate": True},
"loggers": {
"root": {"level": "INFO", "handlers": ["console", "file"], "propagate": True},
"s2python": {"level": "DEBUG", "handlers": ["s2console"], "propagate": False},
"flexmeasures_s2.rm": {
"level": "DEBUG",
"handlers": ["s2rmconsole"],
"propagate": False,
},
},
}


Expand Down
7 changes: 7 additions & 0 deletions flexmeasures/utils/time_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ def server_now() -> datetime:
return datetime.now(get_timezone())


def floored_server_now(resolution: timedelta) -> datetime:
"""Return the current server time floored to the nearest multiple of `resolution`."""
ref = pytz.utc.localize(datetime.min)
_tz = get_timezone()
return (ref + ((server_now() - ref) // resolution) * resolution).astimezone(_tz)


def ensure_local_timezone(
dt: pd.Timestamp | datetime, tz_name: str = "Europe/Amsterdam"
) -> pd.Timestamp | datetime:
Expand Down
18 changes: 18 additions & 0 deletions flexmeasures/ws/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import importlib
import pkgutil
from flask import Blueprint, current_app
from flask_security import auth_token_required

from flask_sock import Sock

sock = Sock()


def import_all_modules(package_name):
package = importlib.import_module(package_name)
for _, name, _ in pkgutil.iter_modules(package.__path__):
importlib.import_module(f"{package_name}.{name}")


# we need to import all the modules to run the route decorators
import_all_modules("flexmeasures.ws")
31 changes: 31 additions & 0 deletions flexmeasures/ws/ping1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging
from flexmeasures.ws import sock
from flask import current_app
from flexmeasures import Sensor
from sqlalchemy import select, func
import uuid
import json

logger = logging.getLogger(__name__)


@sock.route("/ping1")
async def echo1(ws):
headers = ws.environ # Access all headers from the connection
client_id = str(uuid.uuid4())

logger.info("-----------------------------------------")
logger.info(f"Received headers: {headers}")
logger.info("-----------------------------------------")
logger.info(f"Type of ws: {type(ws)}")
logger.info(f"Client ID: {client_id}")
await ws.send(
json.dumps({"type": "metadata", "headers": {"X-Server-Header": "ServerValue"}})
)
while True:
data = await ws.receive()
logger.error("ping1>" + data)
if data == "close":
break
# sensors = current_app.db.session.execute(select(func.count(Sensor.id))).scalar()
await ws.send(data)
14 changes: 14 additions & 0 deletions flexmeasures/ws/ping2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import logging
from flexmeasures.ws import sock

logger = logging.Logger(__name__)


@sock.route("/ping2")
def echo2(ws):
while True:
data = ws.receive()
logger.error("ping2>" + data)
if data == "close":
break
ws.send(data)
Empty file.
31 changes: 31 additions & 0 deletions flexmeasures/ws/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import threading
import time

import pytest
import pytest_asyncio
import websockets
from werkzeug.serving import make_server


@pytest.fixture(scope="module")
def server(app):
"""Run Flask app with Sock in a thread for testing WebSocket"""
srv = make_server("127.0.0.1", 5005, app)
thread = threading.Thread(target=srv.serve_forever)
thread.start()
time.sleep(0.1) # wait for server to start
yield "ws://127.0.0.1:5005"
srv.shutdown()
thread.join()


@pytest_asyncio.fixture
async def connect_to_ws(server):
"""Yield a callable to connect to a given WS endpoint by name."""

async def connect(endpoint_name):
url = f"{server}/{endpoint_name}"
conn = await websockets.connect(url)
return conn

yield connect
24 changes: 24 additions & 0 deletions flexmeasures/ws/tests/test_s2_client_rm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pytest

import logging
import websockets

logger = logging.getLogger("s2python")
SERVER_URL = "ws://127.0.0.1:5000"


@pytest.mark.asyncio
async def test_ping2_echo(connect_to_ws):

# Connect to WS endpoint
ws = await connect_to_ws("ping2")

# Send a message
await ws.send("hello")
resp = await ws.recv()
assert resp == "hello", "echo should return the same message"

# Trigger server-side close
await ws.send("close")
with pytest.raises(websockets.exceptions.ConnectionClosedOK):
await ws.recv(), "expected that, after sending 'close', server breaks loop; connection closes"
38 changes: 38 additions & 0 deletions flexmeasures/ws/v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import logging
from flexmeasures.ws import sock
from flask import current_app
from flexmeasures import Sensor
from sqlalchemy import select, func
import json

logger = logging.Logger(__name__)


@sock.route("/v1")
def header_test(ws):
# Get all headers
all_headers = {
k[5:].lower().replace("_", "-"): v
for k, v in ws.environ.items()
if k.startswith("HTTP_")
}

# Get specific header if needed
custom_header = ws.environ.get("HTTP_X_CUSTOM_HEADER")
# show the type of ws
logger.info(f"Type of ws: {type(ws)}")
logger.info(f"All headers: {all_headers}")
logger.info(f"Custom header: {custom_header}")

# Send initial message with metadata
ws.send(
json.dumps({"type": "metadata", "headers": {"X-Server-Header": "ServerValue"}})
)

while True:
data = ws.receive()
logger.error("v1>" + data)
if data == "close":
break
sensors = current_app.db.session.execute(select(func.count(Sensor.id))).scalar()
ws.send(str(sensors))
Loading
Loading