Skip to content

Commit

Permalink
Implement the DSSEvents interface and fix a few DSSExceptions.
Browse files Browse the repository at this point in the history
  • Loading branch information
PMeira committed Jan 22, 2024
1 parent c161362 commit 14faa62
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 18 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ Check [the Releases page](https://github.com/dss-extensions/dss_python/releases)

Most limitations are inherited from `dss_capi`, i.e., these are not implemented:

- `DSSEvents` from `DLL/ImplEvents.pas`: seems too dependent on COM.
- `DSSProgress` from `DLL/ImplDSSProgress.pas`: would need a reimplementation depending on the target UI (GUI, text, headless, etc.).
- `DSSProgress` from `DLL/ImplDSSProgress.pas`: would need a reimplementation depending on the target UI (GUI, text, headless, etc.). Part of it can already be handled through the callback mechanisms.
- OpenDSS-GIS features are not implemented since they're not open-source.

In general, the DLL from `dss_capi` provides more features than both the official Direct DLL and the COM object.
Expand Down
11 changes: 9 additions & 2 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ relevant. See [DSS C-API's repository](https://github.com/dss-extensions/dss_cap

### 0.14.4 (WIP)

- Add `DSSCompatFlags.ActiveLine`.
- Convert enum comments to docstrings for better user experience.
- Enums:
- Move to DSS-Python-Backend to allow easier sharing among all Python packages from DSS-Extensions.
- Convert enum comments to docstrings for better user experience.
- New `DSSCompatFlags.ActiveLine`.
- New `DSSJSONFlags.SkipTimestamp` and `DSSJSONFlags.SkipBuses`
- New `DSSSaveFlags` (used in the new function `Circuit.Save`)
- New `EnergyMeterRegisters` and `GeneratorRegisters` to simplify handling register indexes from EnergyMeters (`EnergyMeterRegisters`), Generators, PVSystems, and Storage objects (these last three use `GeneratorRegisters`).

- Implement the DSSEvents interface. Note that this is not a popular interface and we haven't seen it used in Python with COM yet. OpenDSS comes with a couple of examples using VBA in Excel though.

### 0.14.3

Expand Down
204 changes: 199 additions & 5 deletions dss/IDSSEvents.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,205 @@
'''
A compatibility layer for DSS C-API that mimics the official OpenDSS COM interface.
Copyright (c) 2016-2019 Paulo Meira
Copyright (c) 2016-2024 Paulo Meira
Copyright (c) 2018-2024 DSS-Extensions contributors
'''
from ._cffi_api_util import Base, DSSException
from .enums import AltDSSEvent
from dss_python_backend.events import get_manager_for_ctx

class DSSEventsConnection:
"""
Wraps a classic DSS Event handler class and its connection state.
This should not be manually created. Use `DSS.Events.WithEvents()`
instead.
"""

def __init__(self, api_util, handler):
EVENTS = (
AltDSSEvent.Legacy_InitControls,
AltDSSEvent.Legacy_CheckControls,
AltDSSEvent.Legacy_StepControls,
)
self._cb_manager = get_manager_for_ctx(api_util.ctx)
self._handler = handler # keep a reference explicitly
self._method_pairs = []
for evt in EVENTS:
evt_name = evt.name.split('_', 1)[1]
func = getattr(handler, evt_name, None)
if func is None:
func = getattr(handler, 'On' + evt_name, None)

if func is None:
raise DSSException(0, f'Events: could not find method for event "{evt_name}".')

self._method_pairs.append((evt, func))

for evt, func in self._method_pairs:
self._cb_manager.register_func(evt, func)

self._connected = True


def close(self):
"""
Alias to disconnect; for compatibility with win32com code.
"""
self.disconnect()


def disconnect(self):
"""
Disconnects the event handler associated to this o
"""
if not self._connected:
raise DSSException(0, 'Events: the event handler has already been disconnected.')

for evt, func in self._method_pairs:
self._cb_manager.unregister_func(evt, func)

self._method_pairs.clear()

self._connected = False


class IDSSEvents(Base):
"""
This interface provides connection to classic the OpenDSS Events
API. For official OpenDSS documentation about this feature, see
the document titled "Evaluation of Distribution Reconfiguration Functions in
Advanced Distribution Management Systems, Example Assessments of Distribution
Automation Using Open Distribution Systems Simulator" (2011), which is available from
EPRI at https://restservice.epri.com/publicdownload/000000000001020090/0/Product
([archived copy here](http://web.archive.org/web/20240121050026/https://restservice.epri.com/publicdownload/000000000001020090/0/Product)).
VBA/Excel examples of the classic COM usage are found in the folder
["Examples/civinlar model/"](https://sourceforge.net/p/electricdss/code/HEAD/tree/trunk/Version8/Distrib/Examples/civinlar%20model/)
([mirrored here](https://github.com/dss-extensions/electricdss-tst/tree/master/Version8/Distrib/Examples/civinlar%20model),
with minor changes), which is distributed along with the official OpenDSS.
For a quick intro, this interface allows connecting an object (event handler)
that runs custom actions are three points of the solution process
(`InitControls`, `StepControls`, `CheckControls`).
Instead of manually writing a full solution loop, users can use this to
connect an event handler to the existing OpenDSS solution loops/algorithms,
which allows some customization opportunities.
Note that AltDSS/DSS C-API provides more/extra events, but this interface is
intended to replace usage of similar `comtypes` and `win32com` features.
That is, see [AltDSS-Python](https://github.com/dss-extensions/altdss-python)
for the extra events, some of which are used by the AltDSS-Python itself to
provide direct-object access.
Since the intention is to allow easy migration (and/or interoperability)
with both the `comtypes` and `win32com` modules, both styles of event handler
classes are allowed:
```python
class EventHandler:
'''An event handler in the style of win32com'''
def OnInitControls(self):
print('Init')
def OnStepControls(self):
print('Step')
def OnCheckControls(self):
print('Check')
# this would be used like:
evt_conn = win32com.client.WithEvents(DSSObj.Events, EventHandler)
```
or
```python
class EventHandler:
'''An event handler in the style of comtypes'''
def InitControls(self):
print('Init')
def StepControls(self):
print('Step')
def CheckControls(self):
print('Check')
# this would be used like:
iface = comtypes.gen.OpenDSSengine.IDSSEventsEvents
evt_conn = comtypes.client.GetEvents(DSSObj.Events, EventHandler(), interface=iface)
```
Like the COM implementations, DSS-Python requires that the three event types are handled.
The method names can be either style (`OnInitControls` or `InitControls`). To make things
easier, there are two methods in our implementation of the Events API that
partially mimic the functions used in the COM modules. Use whichever is
more convenient.
```python
evt_conn = DSSObj.Events.WithEvents(EventHandler) # like win32com, using a class
# or
evt_conn = DSSObj.Events.GetEvents(EventHandler()) # like comtypes, using an object instance
```
"""

class IDSSEvents(object): # Not implemented
__slots__ = []

def __init__(self, api_util):
pass
_columns = []

@staticmethod
def _has_required_methods(obj) -> bool:
has_all_methods = True
has_extra_methods = False
if not (hasattr(obj, 'OnInitControls') or hasattr(obj, 'InitControls')):
has_all_methods = False

if not (hasattr(obj, 'OnStepControls') or hasattr(obj, 'StepControls')):
has_all_methods = False

if not (hasattr(obj, 'OnCheckControls') or hasattr(obj, 'CheckControls')):
has_all_methods = False

if (hasattr(obj, 'OnInitControls') == hasattr(obj, 'InitControls')):
return False

if (hasattr(obj, 'OnStepControls') == hasattr(obj, 'StepControls')):
return False

if (hasattr(obj, 'OnCheckControls') == hasattr(obj, 'CheckControls')):
return False

return has_all_methods


def WithEvents(self, handler_class) -> DSSEventsConnection:
'''
Creates an instance of `handler_class` and connects it to
the classic event system compatibility layer.
This is intended to replace usage of `win32com.client.WithEvents()`
(when previously used with the OpenDSS COM engine).
(API Extension)
'''
if not IDSSEvents._has_required_methods(handler_class):
raise DSSException(0, f'Events: not all event handler methods were found in the provided class ({handler_class}).')

handler_obj = handler_class()
return DSSEventsConnection(self._api_util, handler_obj)


def GetEvents(self, handler_obj) -> DSSEventsConnection:
'''
Connects an object instance to the classic event system compatibility layer.
This is intended to replace usage of `comtypes.client.GetEvents()`
(when previously used with the OpenDSS COM engine).
(API Extension)
'''
if not IDSSEvents._has_required_methods(handler_obj):
raise DSSException(0, f'Events: not all event handler methods were found in the provided object ({handler_obj}).')

return DSSEventsConnection(self._api_util, handler_obj)

1 change: 1 addition & 0 deletions dss/IMonitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def Channel(self, Index: int) -> Float32Array:
num_channels = self.CheckForError(self._lib.Monitors_Get_NumChannels())
if Index < 1 or Index > num_channels:
raise DSSException(
0,
'Monitors.Channel: Invalid channel index ({}), monitor "{}" has {} channels.'.format(
Index, self.Name, num_channels
))
Expand Down
27 changes: 18 additions & 9 deletions dss/_cffi_api_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from ._types import Float64Array, Int32Array, Int8Array, ComplexArray, Float64ArrayOrComplexArray, Float64ArrayOrSimpleComplex
from typing import Any, AnyStr, Callable, List, Union #, Iterator
from .enums import AltDSSEvent
from dss_python_backend.events import get_manager_for_ctx

# UTF8 under testing
codec = 'UTF8'
Expand All @@ -24,6 +25,12 @@ def set_case_insensitive_attributes(use: bool = True, warn: bool = False):
Note that there is a small overhead for allowing case-insensitive names,
thus is not recommended to continue using it after migration/adjustments to
the user code.
Currently, this also affects the new AltDSS package, allowing users to
employ the case-insensitive mechanism to address DSS properties in Python code.
Since there is a small performance overhead, users are recommended to use this
mechanism as a transition before adjusting the code.
'''
if use:
global warn_wrong_case
Expand Down Expand Up @@ -303,15 +310,15 @@ def _decode_and_free_string(self, s) -> str:


def altdss_python_util_callback(ctx, event_code, step, ptr):
# print(ctx, AltDSSEvent(event_code), step, ptr)
util = CffiApiUtil._ctx_to_util[ctx]
# print(ctx_util.ctx, AltDSSEvent(event_code), step, ptr)
ctx_util = CffiApiUtil._ctx_to_util[ctx]

if event_code == AltDSSEvent.ReprocessBuses:
util.reprocess_buses_callback(step)
ctx_util.reprocess_buses_callback(step)
return

if event_code == AltDSSEvent.Clear:
util.clear_callback(step)
ctx_util.clear_callback(step)
return


Expand All @@ -320,7 +327,6 @@ class CffiApiUtil(object):
An internal class with various API and DSSContext management functions and structures.
'''
_ctx_to_util = {}
_altdss_python_util_callback = None

def __init__(self, ffi, lib, ctx=None):
self.owns_ctx = True
Expand All @@ -336,6 +342,9 @@ def __init__(self, ffi, lib, ctx=None):
self._is_clearing = False
if ctx is None:
self.lib = lib
ctx = lib.ctx_Get_Prime()
self.ctx = ctx
CffiApiUtil._ctx_to_util[ctx] = self
else:
self.lib = CtxLib(ctx, ffi, lib)

Expand Down Expand Up @@ -423,11 +432,11 @@ def clear_callback(self, step: int):


def register_callbacks(self):
if CffiApiUtil._altdss_python_util_callback is None:
CffiApiUtil._altdss_python_util_callback = self.ffi.def_extern(name='altdss_python_util_callback')(altdss_python_util_callback)
mgr = get_manager_for_ctx(self.ctx)
# if multiple calls, the extras are ignored
mgr.register_func(AltDSSEvent.Clear, altdss_python_util_callback)
mgr.register_func(AltDSSEvent.ReprocessBuses, altdss_python_util_callback)

self.lib.DSSEvents_RegisterAlt(AltDSSEvent.Clear, self.lib_unpatched.altdss_python_util_callback)
self.lib.DSSEvents_RegisterAlt(AltDSSEvent.ReprocessBuses, self.lib_unpatched.altdss_python_util_callback)

# The context will die, no need to do anything else currently.
def __del__(self):
Expand Down
1 change: 1 addition & 0 deletions dss/altdss/MonitorExtras.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def Channel(self, index: int) -> Float32Array:
num_channels = self.NumChannels()
if index < 1 or index > num_channels:
raise DSSException(
0,
'Monitor Channel: Invalid channel index ({}), monitor "{}" has {} channels.'.format(
index, self.Name, num_channels
))
Expand Down

0 comments on commit 14faa62

Please sign in to comment.