Skip to content

Commit

Permalink
Merge pull request #14 from davidraker/driver_service_work
Browse files Browse the repository at this point in the history
Updates for modular RC.
  • Loading branch information
craig8 authored Oct 3, 2024
2 parents f92c8a8 + 24f00b4 commit 3aa8baa
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 74 deletions.
28 changes: 10 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
# VOLTTRON Fake Driver Interface

![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)
![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)
![Passing?](https://github.com/eclipse-volttron/volttron-lib-fake-driver/actions/workflows/run-tests.yml/badge.svg)
[![pypi version](https://img.shields.io/pypi/v/volttron-lib-fake-driver.svg)](https://pypi.org/project/volttron-lib-fake-driver/)

The FakeDriver is a way to quickly see data published to the message bus in a format that mimics
what a true Driver would produce. This is an extremely simple implementation of the
[VOLTTRON Driver Framework](https://eclipse-volttron.readthedocs.io/en/latest/external-docs/volttron-platform-driver/docs/source/index.html).
[VOLTTRON Driver Framework](https://eclipse-volttron.readthedocs.io/en/latest/external-docs/volttron-platform-driver/index.html).
This driver does not connect to any actual device and instead produces random and or pre-configured values.

# Requires

* python >= 3.10
* volttron >= 10.0
* volttron-lib-base-driver
* volttron-core >= 2.0.0rc0
* volttron-lib-base-driver >= 2.0.0rc0


# Documentation
More detailed documentation can be found on [ReadTheDocs](https://eclipse-volttron.readthedocs.io/en/latest/external-docs/volttron-platform-driver/docs/source/index.html).
The RST source of the documentation for this component is located in the "docs" directory of this repository.
More detailed documentation can be found on [ReadTheDocs](https://eclipse-volttron.readthedocs.io/en/latest/external-docs/volttron-lib-fake-driver_docs_root/docs/source/index.html#fake-driver). The RST source
of the documentation for this component is located in the "docs" directory of this repository.


# Installation
Expand All @@ -31,25 +30,18 @@ Information on how to install of the VOLTTRON platform can be found
1. If it is not already, install the VOLTTRON Platform Driver Agent:

```shell
vctl install volttron-platform-driver --vip-identity platform.driver --start
vctl install volttron-platform-driver --vip-identity platform.driver
```

2. Install the volttron fake driver library:

```shell
pip install volttron-lib-fake-driver
poetry add --directory $VOLTTRON_HOME volttron-lib-fake-driver
```

3. Store device and registry files for the Fake device to the Platform Driver configuration store:
3. Create configurations for a fake device:

* Create a config directory and navigate to it:

```shell
mkdir config
cd config
```

* Navigate to the config directory and create a file called `fake.config` and add the following JSON to it:
* Create a file called `fake.config` and add the following JSON to it:

```json
{
Expand Down Expand Up @@ -104,7 +96,7 @@ Information on how to install of the VOLTTRON platform can be found

4. Observe Data

To see data being published to the bus, install a [Listener Agent](https://pypi.org/project/volttron-listener/):
To see data being published to the bus, install a [Listener Agent](https://github.com/eclipse-volttron/volttron-listener):

```
vctl install volttron-listener --start
Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ profile = "black"

[tool.poetry]
name = "volttron-lib-fake-driver"
version = "0.2.0-rc"
version = "2.0.0rc0"
description = "A minimal implementation of a driver for the VOLTTRON platform."
authors = ["VOLTTRON Team <[email protected]>"]
license = "Apache License 2.0"
Expand All @@ -27,10 +27,9 @@ classifiers = [

[tool.poetry.dependencies]
python = ">=3.10,<4.0"
volttron-lib-base-driver = "^0.2.1rc2"
volttron-lib-base-driver = ">=2.0.0rc0"

[tool.poetry.group.dev.dependencies]
volttron-testing = "^0.4.0rc0"
pytest = "^6.2.5"
pytest-cov = "^3.0.0"
mock = "^4.0.3"
Expand Down
113 changes: 60 additions & 53 deletions src/volttron/driver/interfaces/fake/fake.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@
import logging
import math
import random

from collections.abc import KeysView
from math import pi
from pydantic import Field

from volttron.driver.base.interfaces import (BaseInterface, BaseRegister, BasicRevert)
from volttron.driver.base.config import PointConfig, RemoteConfig

_log = logging.getLogger(__name__)
type_mapping = {
Expand All @@ -40,12 +44,21 @@
"boolean": bool
}

class FakeRemoteConfig(RemoteConfig):
remote_id: str | None = None


class FakePointConfig(PointConfig):
# TODO: string starting_value.
starting_value: int | float | bool | str = Field(default='sin', alias='Starting Value')
type: str = Field(default='string', alias='Type')


class FakeRegister(BaseRegister):

def __init__(self, read_only, pointName, units, reg_type, default_value=None, description=''):
def __init__(self, read_only, point_name, units, reg_type, default_value=None, description=''):
# register_type, read_only, pointName, units, description = ''):
super(FakeRegister, self).__init__("byte", read_only, pointName, units, description='')
super(FakeRegister, self).__init__("byte", read_only, point_name, units, description='')
self.reg_type = reg_type

if default_value is None:
Expand All @@ -59,8 +72,8 @@ def __init__(self, read_only, pointName, units, reg_type, default_value=None, de

class EKGregister(BaseRegister):

def __init__(self, read_only, pointName, units, reg_type, default_value=None, description=''):
super(EKGregister, self).__init__("byte", read_only, pointName, units, description='')
def __init__(self, read_only, point_name, units, reg_type, default_value=None, description=''):
super(EKGregister, self).__init__("byte", read_only, point_name, units, description='')
self._value = 1

math_functions = ('acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'sin',
Expand Down Expand Up @@ -88,63 +101,57 @@ def value(self, x):

class Fake(BasicRevert, BaseInterface):

def __init__(self, **kwargs):
super(Fake, self).__init__(**kwargs)

def configure(self, config_dict, registry_config_str):
self.parse_config(registry_config_str)
REGISTER_CONFIG_CLASS = FakePointConfig
INTERFACE_CONFIG_CLASS = FakeRemoteConfig

def get_point(self, point_name):
register = self.get_register_by_name(point_name)
def __init__(self, config: FakeRemoteConfig, *args, **kwargs):
BasicRevert.__init__(self, **kwargs)
BaseInterface.__init__(self, config, *args, **kwargs)

def get_point(self, point_name, **kwargs):
register: FakeRegister = self.get_register_by_name(point_name)
return register.value

def _get_multiple_points(self, topics: KeysView[str], **kwargs) -> (dict, dict):
return BaseInterface.get_multiple_points(self, topics, **kwargs)

def _set_point(self, point_name, value):
register = self.get_register_by_name(point_name)
register: FakeRegister = self.get_register_by_name(point_name)
if register.read_only:
raise RuntimeError("Trying to write to a point configured read only: " + point_name)

register.value = register.reg_type(value)
return register.value

def _scrape_all(self):
result = {}
read_registers = self.get_registers_by_type("byte", True)
write_registers = self.get_registers_by_type("byte", False)
for register in read_registers + write_registers:
result[register.point_name] = register.value

return result

def parse_config(self, configDict):
if configDict is None:
return

for regDef in configDict:
# Skip lines that have no address yet.
if not regDef['Point Name']:
continue

read_only = regDef['Writable'].lower() != 'true'
point_name = regDef['Volttron Point Name']
description = regDef.get('Notes', '')
units = regDef['Units']
default_value = regDef.get("Starting Value", 'sin').strip()
if not default_value:
default_value = None
type_name = regDef.get("Type", 'string')
reg_type = type_mapping.get(type_name, str)

register_type = FakeRegister if not point_name.startswith('EKG') else EKGregister

register = register_type(read_only,
point_name,
units,
reg_type,
default_value=default_value,
description=description)

if default_value is not None:
self.set_default(point_name, register.value)

self.insert_register(register)
def create_register(self, register_definition: FakePointConfig) -> FakeRegister:
read_only = register_definition.writable is not True
description = register_definition.notes
units = register_definition.units
default_value = register_definition.starting_value.strip()
if not default_value:
default_value = None
reg_type = type_mapping.get(register_definition.type, str)

register_type = FakeRegister if not register_definition.volttron_point_name.startswith(
'EKG') else EKGregister

register = register_type(read_only,
register_definition.volttron_point_name,
units,
reg_type,
default_value=default_value,
description=description)

if default_value is not None:
self.set_default(register_definition.volttron_point_name, register.value)
return register

@classmethod
def unique_remote_id(cls, config_name: str, config: RemoteConfig) -> tuple:
"""Unique Remote ID
Subclasses should use this class method to return a hashable identifier which uniquely identifies a single
remote -- e.g., if multiple remotes may exist at a single IP address, but on different ports,
the unique ID might be the tuple: (ip_address, port).
The base class returns the name of the device configuration file, requiring a separate DriverAgent for each.
"""
return (config_name,) if config.remote_id is None else (config.remote_id,)

0 comments on commit 3aa8baa

Please sign in to comment.