This is an automated email from the ASF dual-hosted git repository.
hutcheb pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
The following commit(s) were added to refs/heads/develop by this push:
new ce60af02f5 fix(plc4py): added PlcDriver and moved Mock Driver to plugin
ce60af02f5 is described below
commit ce60af02f545254eedd76c4390b5dbc710c56acf
Author: Ben Hutcheson <[email protected]>
AuthorDate: Thu Apr 28 07:04:34 2022 +1000
fix(plc4py): added PlcDriver and moved Mock Driver to plugin
* Updated the Mock Connection and changed it to a driver module
* Updated PlcValue interface to include stub methods for get_*
* Add some doc strings
* Add test for mock read request
* Fixed the way futures are created
It now uses asyncio.ensure_future and doesn't rely on the explicitly
setting a result
* Realized I missed the PlcDriver class and went straight to PlcConnection
* Finished including PlcDriver
---
sandbox/plc4py/plc4py/PlcDriverManager.py | 11 +-
sandbox/plc4py/plc4py/api/PlcConnection.py | 23 ++-
sandbox/plc4py/plc4py/api/PlcDriver.py | 70 ++++++++
.../api/authentication/PlcAuthentication.py} | 4 +
.../test => plc4py/api/authentication}/__init__.py | 0
sandbox/plc4py/plc4py/api/exceptions/exceptions.py | 8 +
.../api/messages/{PlcField.py => PlcDiscovery.py} | 10 +-
sandbox/plc4py/plc4py/api/messages/PlcField.py | 13 ++
sandbox/plc4py/plc4py/api/value/PlcValue.py | 6 +
.../{PlcConnectionLoader.py => PlcDriverLoader.py} | 6 +-
.../plc4py/plc4py/drivers/mock/MockConnection.py | 197 +++++++++++++++++++++
.../drivers/mock}/MockReadRequestBuilder.py | 0
.../api/test => plc4py/drivers/mock}/__init__.py | 0
.../plc4py/drivers/modbus/ModbusConnection.py | 27 ++-
sandbox/plc4py/plc4py/spi/PlcDriverClassLoader.py | 4 +-
.../PlcReader.py} | 28 ++-
sandbox/plc4py/setup.py | 3 +-
sandbox/plc4py/tests/test_plc4py.py | 28 +++
.../tests/unit/plc4py/api/test/MockPlcConection.py | 83 ---------
.../tests/unit/plc4py/api/test_PlcRequest.py | 64 ++++++-
.../tests/unit/plc4py/test_PlcDriverManager.py | 4 +-
21 files changed, 462 insertions(+), 127 deletions(-)
diff --git a/sandbox/plc4py/plc4py/PlcDriverManager.py
b/sandbox/plc4py/plc4py/PlcDriverManager.py
index 8f67acb7df..d5df488252 100644
--- a/sandbox/plc4py/plc4py/PlcDriverManager.py
+++ b/sandbox/plc4py/plc4py/PlcDriverManager.py
@@ -24,6 +24,7 @@ from typing import Generator, Type
from pluggy import PluginManager # type: ignore
from plc4py.api.PlcConnection import PlcConnection
+from plc4py.api.PlcDriver import PlcDriver
from plc4py.spi.PlcDriverClassLoader import PlcDriverClassLoader
from plc4py.utils.ConnectionStringHandling import get_protocol_code
@@ -31,7 +32,7 @@ from plc4py.utils.ConnectionStringHandling import
get_protocol_code
@dataclass
class PlcDriverManager:
class_loader: PluginManager = field(default_factory=lambda:
PluginManager("plc4py"))
- _driverMap: dict[str, Type[PlcConnection]] = field(default_factory=lambda:
{})
+ _driverMap: dict[str, Type[PlcDriver]] = field(default_factory=lambda: {})
def __post_init__(self):
logging.info(
@@ -47,7 +48,7 @@ class PlcDriverManager:
self._driverMap = {
key: loader
for key, loader in zip(
- self.class_loader.hook.key(),
self.class_loader.hook.get_connection()
+ self.class_loader.hook.key(),
self.class_loader.hook.get_driver()
)
}
for driver in self._driverMap:
@@ -77,7 +78,7 @@ class PlcDriverManager:
:return: plc connection
"""
protocol_code = get_protocol_code(url)
- return self._driverMap[protocol_code](url)
+ return self._driverMap[protocol_code]().get_connection(url)
def list_drivers(self) -> list[str]:
"""
@@ -86,7 +87,7 @@ class PlcDriverManager:
"""
return list(self._driverMap.keys())
- def get_driver(self, protocol_code: str) -> Type[PlcConnection]:
+ def get_driver(self, protocol_code: str) -> Type[PlcDriver]:
"""
Returns suitable driver for protocol or throws an Exception.
:param protocol_code: protocolCode protocol code identifying the driver
@@ -94,7 +95,7 @@ class PlcDriverManager:
"""
return self._driverMap[protocol_code]
- def get_driver_for_url(self, url: str) -> Type[PlcConnection]:
+ def get_driver_for_url(self, url: str) -> Type[PlcDriver]:
"""
Returns the driver class that matches that identified within the
connection string
:param url: The plc connection string
diff --git a/sandbox/plc4py/plc4py/api/PlcConnection.py
b/sandbox/plc4py/plc4py/api/PlcConnection.py
index 52c439ccc0..7f85ea0f88 100644
--- a/sandbox/plc4py/plc4py/api/PlcConnection.py
+++ b/sandbox/plc4py/plc4py/api/PlcConnection.py
@@ -16,12 +16,14 @@
# specific language governing permissions and limitations
# under the License.
#
+import asyncio
from abc import abstractmethod
from typing import Awaitable
-from plc4py.api.messages.PlcResponse import PlcResponse
-from plc4py.api.messages.PlcRequest import ReadRequestBuilder
+from plc4py.api.messages.PlcResponse import PlcResponse, PlcReadResponse
+from plc4py.api.messages.PlcRequest import ReadRequestBuilder, PlcRequest
from plc4py.api.exceptions.exceptions import PlcConnectionException
+from plc4py.api.value.PlcValue import PlcResponseCode
from plc4py.utils.GenericTypes import GenericGenerator
@@ -60,10 +62,23 @@ class PlcConnection(GenericGenerator):
pass
@abstractmethod
- def execute(self, PlcRequest) -> Awaitable[PlcResponse]:
+ def execute(self, request: PlcRequest) -> Awaitable[PlcResponse]:
"""
Executes a PlcRequest as long as it's already connected
- :param PlcRequest: Plc Request to execute
+ :param request: Plc Request to execute
:return: The response from the Plc/Device
"""
pass
+
+ def _default_failed_request(
+ self, code: PlcResponseCode
+ ) -> Awaitable[PlcReadResponse]:
+ """
+ Returns a default PlcResponse, mainly used in case of a failed request
+ :param code: The response code to return
+ :return: The PlcResponse
+ """
+ loop = asyncio.get_running_loop()
+ future = loop.create_future()
+ future.set_result(PlcResponse(code))
+ return future
diff --git a/sandbox/plc4py/plc4py/api/PlcDriver.py
b/sandbox/plc4py/plc4py/api/PlcDriver.py
new file mode 100644
index 0000000000..e7ea76bf8b
--- /dev/null
+++ b/sandbox/plc4py/plc4py/api/PlcDriver.py
@@ -0,0 +1,70 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+from abc import abstractmethod
+from dataclasses import dataclass
+
+from plc4py.api.PlcConnection import PlcConnection
+from plc4py.api.authentication.PlcAuthentication import PlcAuthentication
+from plc4py.api.exceptions.exceptions import PlcNotImplementedException
+from plc4py.api.messages.PlcDiscovery import PlcDiscoveryRequestBuilder
+
+
+@dataclass
+class PlcDriverMetaData:
+ """
+ Information about driver capabilities
+ """
+
+ """Indicates that the driver supports discovery"""
+ can_discover: bool = False
+
+
+class PlcDriver:
+ """
+ General interface defining the minimal methods required for adding a new
type of driver to the PLC4PY system.
+
+ <b>Note that each driver has to add a setuptools entrypoint as
plc4x.driver in order to be loaded by pluggy</b>
+ """
+
+ def __init__(self):
+ self.protocol_code: str
+ self.protocol_name: str
+
+ @property
+ def metadata(self):
+ return PlcDriverMetaData()
+
+ @abstractmethod
+ def get_connection(
+ self, url: str, authentication: PlcAuthentication = PlcAuthentication()
+ ) -> PlcConnection:
+ """
+ Connects to a PLC using the given plc connection string.
+ :param url: plc connection string
+ :param authentication: authentication credentials.
+ :return PlcConnection: PLC Connection object
+ """
+ pass
+
+ def discovery_request_builder(self) -> PlcDiscoveryRequestBuilder:
+ """
+ Discovery Request Builder aids in generating a discovery request for
this protocol
+ :return builder: Discovery request builder
+ """
+ raise PlcNotImplementedException(f"Not implemented for
{self.protocol_name}")
diff --git a/sandbox/plc4py/tests/unit/plc4py/api/test/__init__.py
b/sandbox/plc4py/plc4py/api/authentication/PlcAuthentication.py
similarity index 95%
copy from sandbox/plc4py/tests/unit/plc4py/api/test/__init__.py
copy to sandbox/plc4py/plc4py/api/authentication/PlcAuthentication.py
index 585be9602f..c2b409f19b 100644
--- a/sandbox/plc4py/tests/unit/plc4py/api/test/__init__.py
+++ b/sandbox/plc4py/plc4py/api/authentication/PlcAuthentication.py
@@ -16,3 +16,7 @@
# specific language governing permissions and limitations
# under the License.
#
+
+
+class PlcAuthentication:
+ pass
diff --git a/sandbox/plc4py/tests/unit/plc4py/api/test/__init__.py
b/sandbox/plc4py/plc4py/api/authentication/__init__.py
similarity index 100%
copy from sandbox/plc4py/tests/unit/plc4py/api/test/__init__.py
copy to sandbox/plc4py/plc4py/api/authentication/__init__.py
diff --git a/sandbox/plc4py/plc4py/api/exceptions/exceptions.py
b/sandbox/plc4py/plc4py/api/exceptions/exceptions.py
index 7a7eb4647b..2d41acd8bc 100644
--- a/sandbox/plc4py/plc4py/api/exceptions/exceptions.py
+++ b/sandbox/plc4py/plc4py/api/exceptions/exceptions.py
@@ -25,3 +25,11 @@ class PlcException(Exception):
class PlcConnectionException(Exception):
logging.error("Unable to establish a connection to the plc")
+
+
+class PlcFieldParseException(Exception):
+ pass
+
+
+class PlcNotImplementedException(Exception):
+ pass
diff --git a/sandbox/plc4py/plc4py/api/messages/PlcField.py
b/sandbox/plc4py/plc4py/api/messages/PlcDiscovery.py
similarity index 90%
copy from sandbox/plc4py/plc4py/api/messages/PlcField.py
copy to sandbox/plc4py/plc4py/api/messages/PlcDiscovery.py
index fce052b1ab..04049f82b1 100644
--- a/sandbox/plc4py/plc4py/api/messages/PlcField.py
+++ b/sandbox/plc4py/plc4py/api/messages/PlcDiscovery.py
@@ -16,9 +16,11 @@
# specific language governing permissions and limitations
# under the License.
#
-from dataclasses import dataclass
-@dataclass
-class PlcField:
- name: str
+class PlcDiscoveryRequest:
+ pass
+
+
+class PlcDiscoveryRequestBuilder:
+ pass
diff --git a/sandbox/plc4py/plc4py/api/messages/PlcField.py
b/sandbox/plc4py/plc4py/api/messages/PlcField.py
index fce052b1ab..57ed476ddc 100644
--- a/sandbox/plc4py/plc4py/api/messages/PlcField.py
+++ b/sandbox/plc4py/plc4py/api/messages/PlcField.py
@@ -21,4 +21,17 @@ from dataclasses import dataclass
@dataclass
class PlcField:
+ """
+ Base type for all field types.
+ Typically every driver provides an implementation of this interface in
order
+ to be able to describe the fields of a resource. As this is completely
tied to
+ the implemented protocol, this base interface makes absolutely no
assumption to
+ any information it should provide.
+
+ In order to stay platform and protocol independent every driver connection
implementation
+ provides a prepareField(String) method that is able to parse a string
representation of
+ a resource into it's individual field type. Manually constructing PlcField
objects
+ manually makes the solution less independent from the protocol, but might
be faster.
+ """
+
name: str
diff --git a/sandbox/plc4py/plc4py/api/value/PlcValue.py
b/sandbox/plc4py/plc4py/api/value/PlcValue.py
index 6e2bd59d4b..bded459b4f 100644
--- a/sandbox/plc4py/plc4py/api/value/PlcValue.py
+++ b/sandbox/plc4py/plc4py/api/value/PlcValue.py
@@ -28,6 +28,12 @@ T = TypeVar("T")
class PlcValue(Generic[T], ABC):
value: T
+ def get_bool(self):
+ return self.value
+
+ def get_int(self):
+ return self.value
+
class PlcResponseCode(Enum):
OK = auto()
diff --git a/sandbox/plc4py/plc4py/drivers/PlcConnectionLoader.py
b/sandbox/plc4py/plc4py/drivers/PlcDriverLoader.py
similarity index 91%
rename from sandbox/plc4py/plc4py/drivers/PlcConnectionLoader.py
rename to sandbox/plc4py/plc4py/drivers/PlcDriverLoader.py
index cc8b757ad7..921bcf2b86 100644
--- a/sandbox/plc4py/plc4py/drivers/PlcConnectionLoader.py
+++ b/sandbox/plc4py/plc4py/drivers/PlcDriverLoader.py
@@ -19,10 +19,10 @@
from abc import abstractmethod
from typing import Type
-from plc4py.api.PlcConnection import PlcConnection
+from plc4py.api.PlcDriver import PlcDriver
-class PlcConnectionLoader:
+class PlcDriverLoader:
"""
Abstract class for Plc Driver Loaders.
Each method should use the @hookimpl decorator to indicate it is a driver
loader
@@ -30,7 +30,7 @@ class PlcConnectionLoader:
@staticmethod
@abstractmethod
- def get_connection() -> Type[PlcConnection]:
+ def get_driver() -> Type[PlcDriver]:
"""
:return Type[PlcConnection]: Returns the PlcConnection class that is
used to instantiate the driver
"""
diff --git a/sandbox/plc4py/plc4py/drivers/mock/MockConnection.py
b/sandbox/plc4py/plc4py/drivers/mock/MockConnection.py
new file mode 100644
index 0000000000..ce3cc98bf8
--- /dev/null
+++ b/sandbox/plc4py/plc4py/drivers/mock/MockConnection.py
@@ -0,0 +1,197 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+import asyncio
+import logging
+from dataclasses import dataclass, field
+from typing import Awaitable, Type
+
+import plc4py
+
+from plc4py.api.PlcConnection import PlcConnection
+from plc4py.api.PlcDriver import PlcDriver
+from plc4py.api.authentication.PlcAuthentication import PlcAuthentication
+from plc4py.api.exceptions.exceptions import PlcFieldParseException
+from plc4py.api.messages.PlcField import PlcField
+from plc4py.api.messages.PlcRequest import (
+ ReadRequestBuilder,
+ PlcReadRequest,
+ PlcRequest,
+)
+from plc4py.api.messages.PlcResponse import PlcReadResponse, PlcResponse
+from plc4py.api.value.PlcValue import PlcResponseCode, PlcValue
+from plc4py.drivers.PlcDriverLoader import PlcDriverLoader
+from plc4py.spi.messages.PlcReader import PlcReader
+from plc4py.spi.messages.utils.ResponseItem import ResponseItem
+from plc4py.spi.values.PlcBOOL import PlcBOOL
+from plc4py.spi.values.PlcINT import PlcINT
+from plc4py.drivers.mock.MockReadRequestBuilder import MockReadRequestBuilder
+
+
+@dataclass
+class MockPlcField(PlcField):
+ """
+ Mock PLC Field type
+ """
+
+ datatype: str = "INT"
+
+
+class MockPlcFieldHandler:
+ """
+ Helper class to generate MockPlcField based on a fieldquery
+ """
+
+ @staticmethod
+ def of(fieldquery: str) -> MockPlcField:
+ """
+ :param fieldquery: Field identifier string e.g. '1:BOOL'
+ :return: A MockPlcField with the datatype populated
+ """
+ try:
+ datatype = fieldquery.split(":")[1]
+ return MockPlcField(fieldquery, datatype)
+ except IndexError:
+ raise PlcFieldParseException
+
+
+@dataclass
+class MockDevice:
+ fields: dict[str, PlcValue] = field(default_factory=lambda: {})
+
+ def read(self, field: str) -> list[ResponseItem[PlcValue]]:
+ """
+ Reads one field from the Mock Device
+ """
+ logging.debug(f"Reading field {field} from Mock Device")
+ plc_field = MockPlcFieldHandler.of(field)
+ if plc_field.datatype == "BOOL":
+ self.fields[field] = PlcBOOL(False)
+ return [ResponseItem(PlcResponseCode.OK, self.fields[field])]
+ elif plc_field.datatype == "INT":
+ self.fields[field] = PlcINT(0)
+ return [ResponseItem(PlcResponseCode.OK, self.fields[field])]
+ else:
+ raise PlcFieldParseException
+
+
+@dataclass
+class MockConnection(PlcConnection, PlcReader):
+ _is_connected: bool = False
+ device: MockDevice = field(default_factory=lambda: MockDevice())
+
+ def connect(self):
+ """
+ Connect the Mock PLC connection
+ :return:
+ """
+ self._is_connected = True
+ self.device = MockDevice()
+
+ def is_connected(self) -> bool:
+ """
+ Return the current status of the Mock PLC Connection
+ :return bool: True is connected
+ """
+ return self._is_connected
+
+ def close(self) -> None:
+ """
+ Closes the connection to the remote PLC.
+ :return:
+ """
+ self._is_connected = False
+
+ def read_request_builder(self) -> ReadRequestBuilder:
+ """
+ :return: read request builder.
+ """
+ return MockReadRequestBuilder()
+
+ def execute(self, request: PlcRequest) -> Awaitable[PlcResponse]:
+ """
+ Executes a PlcRequest as long as it's already connected
+ :param PlcRequest: Plc Request to execute
+ :return: The response from the Plc/Device
+ """
+ if not self.is_connected():
+ return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)
+
+ if isinstance(request, PlcReadRequest):
+ return self._read(request)
+
+ return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)
+
+ def _read(self, request: PlcReadRequest) -> Awaitable[PlcReadResponse]:
+ """
+ Executes a PlcReadRequest
+ """
+ if self.device is None:
+ logging.error("No device is set in the mock connection!")
+ return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)
+
+ async def _request(req, device) -> PlcReadResponse:
+ try:
+ response = PlcReadResponse(
+ PlcResponseCode.OK,
+ req.fields,
+ {field: device.read(field) for field in req.field_names},
+ )
+ return response
+ except Exception:
+ # TODO:- This exception is very general and probably should be
replaced
+ return PlcReadResponse(PlcResponseCode.INTERNAL_ERROR,
req.fields, {})
+
+ logging.debug("Sending read request to MockDevice")
+ future = asyncio.ensure_future(_request(request, self.device))
+ return future
+
+
+class MockDriver(PlcDriver):
+ def __init__(self):
+ self.protocol_code = "mock"
+ self.protocol_name = "Mock"
+
+ def get_connection(
+ self, url: str, authentication: PlcAuthentication = PlcAuthentication()
+ ) -> PlcConnection:
+ """
+ Connects to a PLC using the given plc connection string.
+ :param url: plc connection string
+ :param authentication: authentication credentials.
+ :return PlcConnection: PLC Connection object
+ """
+ return MockConnection()
+
+
+class MockDriverLoader(PlcDriverLoader):
+ """
+ Mock Connection Loader, after adding this to the setuptools entry point
+ pluggy should be able to find this and import it.
+ """
+
+ @staticmethod
+ @plc4py.hookimpl
+ def get_driver() -> Type[MockDriver]:
+ return MockDriver
+
+ @staticmethod
+ @plc4py.hookimpl
+ def key() -> str:
+ return "mock"
diff --git
a/sandbox/plc4py/tests/unit/plc4py/api/test/MockReadRequestBuilder.py
b/sandbox/plc4py/plc4py/drivers/mock/MockReadRequestBuilder.py
similarity index 100%
rename from sandbox/plc4py/tests/unit/plc4py/api/test/MockReadRequestBuilder.py
rename to sandbox/plc4py/plc4py/drivers/mock/MockReadRequestBuilder.py
diff --git a/sandbox/plc4py/tests/unit/plc4py/api/test/__init__.py
b/sandbox/plc4py/plc4py/drivers/mock/__init__.py
similarity index 100%
rename from sandbox/plc4py/tests/unit/plc4py/api/test/__init__.py
rename to sandbox/plc4py/plc4py/drivers/mock/__init__.py
diff --git a/sandbox/plc4py/plc4py/drivers/modbus/ModbusConnection.py
b/sandbox/plc4py/plc4py/drivers/modbus/ModbusConnection.py
index c4f14e8b29..7eba4e3ea6 100644
--- a/sandbox/plc4py/plc4py/drivers/modbus/ModbusConnection.py
+++ b/sandbox/plc4py/plc4py/drivers/modbus/ModbusConnection.py
@@ -20,9 +20,11 @@ from typing import Type, Awaitable
import plc4py
from plc4py.api.PlcConnection import PlcConnection
+from plc4py.api.PlcDriver import PlcDriver
+from plc4py.api.authentication.PlcAuthentication import PlcAuthentication
from plc4py.api.messages.PlcResponse import PlcResponse
from plc4py.api.messages.PlcRequest import ReadRequestBuilder
-from plc4py.drivers.PlcConnectionLoader import PlcConnectionLoader
+from plc4py.drivers.PlcDriverLoader import PlcDriverLoader
class ModbusConnection(PlcConnection):
@@ -66,11 +68,28 @@ class ModbusConnection(PlcConnection):
pass
-class ModbusConnectionLoader(PlcConnectionLoader):
+class ModbusDriver(PlcDriver):
+ def __init__(self):
+ self.protocol_code = "modbus"
+ self.protocol_name = "Modbus"
+
+ def get_connection(
+ self, url: str, authentication: PlcAuthentication = PlcAuthentication()
+ ) -> PlcConnection:
+ """
+ Connects to a PLC using the given plc connection string.
+ :param url: plc connection string
+ :param authentication: authentication credentials.
+ :return PlcConnection: PLC Connection object
+ """
+ return ModbusConnection(url)
+
+
+class ModbusDriverLoader(PlcDriverLoader):
@staticmethod
@plc4py.hookimpl
- def get_connection() -> Type[ModbusConnection]:
- return ModbusConnection
+ def get_driver() -> Type[ModbusDriver]:
+ return ModbusDriver
@staticmethod
@plc4py.hookimpl
diff --git a/sandbox/plc4py/plc4py/spi/PlcDriverClassLoader.py
b/sandbox/plc4py/plc4py/spi/PlcDriverClassLoader.py
index a500aceef0..76901feb8d 100644
--- a/sandbox/plc4py/plc4py/spi/PlcDriverClassLoader.py
+++ b/sandbox/plc4py/plc4py/spi/PlcDriverClassLoader.py
@@ -20,7 +20,7 @@ from typing import Type
import pluggy
-from plc4py.api.PlcConnection import PlcConnection
+from plc4py.api.PlcDriver import PlcDriver
class PlcDriverClassLoader:
@@ -30,7 +30,7 @@ class PlcDriverClassLoader:
@staticmethod
@hookspec
- def get_connection() -> Type[PlcConnection]:
+ def get_driver() -> Type[PlcDriver]:
"""Returns the PlcConnection class that is used to instantiate the
driver"""
@staticmethod
diff --git a/sandbox/plc4py/plc4py/spi/PlcDriverClassLoader.py
b/sandbox/plc4py/plc4py/spi/messages/PlcReader.py
similarity index 58%
copy from sandbox/plc4py/plc4py/spi/PlcDriverClassLoader.py
copy to sandbox/plc4py/plc4py/spi/messages/PlcReader.py
index a500aceef0..5f1b993e05 100644
--- a/sandbox/plc4py/plc4py/spi/PlcDriverClassLoader.py
+++ b/sandbox/plc4py/plc4py/spi/messages/PlcReader.py
@@ -16,24 +16,22 @@
# specific language governing permissions and limitations
# under the License.
#
-from typing import Type
-import pluggy
+from typing import Awaitable
-from plc4py.api.PlcConnection import PlcConnection
+from plc4py.api.messages.PlcRequest import PlcReadRequest
+from plc4py.api.messages.PlcResponse import PlcReadResponse
-class PlcDriverClassLoader:
- """Hook spec for PLC4PY Driver Loaders"""
+class PlcReader:
+ """
+ Interface implemented by all PlcConnections that are able to read from
remote resources.
+ """
- hookspec = pluggy.HookspecMarker("plc4py")
+ def _read(self, request: PlcReadRequest) -> Awaitable[PlcReadResponse]:
+ """
+ Reads a requested value from a PLC
- @staticmethod
- @hookspec
- def get_connection() -> Type[PlcConnection]:
- """Returns the PlcConnection class that is used to instantiate the
driver"""
-
- @staticmethod
- @hookspec
- def key() -> str:
- """Unique key to identify the driver"""
+ :param request: object describing the type and location of the value
+ :return: Future, giving async access to the returned value
+ """
diff --git a/sandbox/plc4py/setup.py b/sandbox/plc4py/setup.py
index 9bb84aa512..8d36690742 100644
--- a/sandbox/plc4py/setup.py
+++ b/sandbox/plc4py/setup.py
@@ -51,7 +51,8 @@ setup(
},
entry_points={
"plc4py.drivers": [
- "modbus =
plc4py.drivers.modbus.ModbusConnection:ModbusConnectionLoader"
+ "mock = plc4py.drivers.mock.MockConnection:MockDriverLoader",
+ "modbus =
plc4py.drivers.modbus.ModbusConnection:ModbusDriverLoader",
]
},
)
diff --git a/sandbox/plc4py/tests/test_plc4py.py
b/sandbox/plc4py/tests/test_plc4py.py
index 5427c832b5..bc735b61cd 100644
--- a/sandbox/plc4py/tests/test_plc4py.py
+++ b/sandbox/plc4py/tests/test_plc4py.py
@@ -16,10 +16,15 @@
# specific language governing permissions and limitations
# under the License.
#
+from typing import cast
from plc4py import __version__
from plc4py.PlcDriverManager import PlcDriverManager
from plc4py.api.PlcConnection import PlcConnection
+from plc4py.api.messages.PlcRequest import PlcFieldRequest
+from plc4py.api.messages.PlcResponse import PlcReadResponse
+from plc4py.api.value.PlcValue import PlcResponseCode
+from plc4py.drivers.mock.MockConnection import MockConnection
from plc4py.drivers.modbus.ModbusConnection import ModbusConnection
@@ -37,3 +42,26 @@ def test_plc_driver_manager_init_modbus():
driver_manager = PlcDriverManager()
with driver_manager.connection("modbus:tcp://127.0.0.1:502") as connection:
assert isinstance(connection, ModbusConnection)
+
+
+def test_plc_driver_manager_init_mock():
+ driver_manager = PlcDriverManager()
+ with driver_manager.connection("mock:tcp://127.0.0.1:502") as connection:
+ assert isinstance(connection, MockConnection)
+
+
+async def test_plc_driver_manager_init_mock_read_request():
+ driver_manager = PlcDriverManager()
+ field = "1:BOOL"
+
+ with driver_manager.connection("mock:tcp://127.0.0.1:502") as connection:
+ connection.connect()
+ with connection.read_request_builder() as builder:
+ builder.add_item(field)
+ request: PlcFieldRequest = builder.build()
+ response: PlcReadResponse = cast(
+ PlcReadResponse, await connection.execute(request)
+ )
+
+ # verify that request has one field
+ assert response.code == PlcResponseCode.OK
diff --git a/sandbox/plc4py/tests/unit/plc4py/api/test/MockPlcConection.py
b/sandbox/plc4py/tests/unit/plc4py/api/test/MockPlcConection.py
deleted file mode 100644
index 0e52c49d2f..0000000000
--- a/sandbox/plc4py/tests/unit/plc4py/api/test/MockPlcConection.py
+++ /dev/null
@@ -1,83 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-#
-import asyncio
-from dataclasses import dataclass
-from typing import Awaitable
-
-from plc4py.api.PlcConnection import PlcConnection
-from plc4py.api.messages.PlcRequest import ReadRequestBuilder, PlcReadRequest
-from plc4py.api.messages.PlcResponse import PlcReadResponse, PlcResponse
-from plc4py.api.value.PlcValue import PlcResponseCode
-from tests.unit.plc4py.api.test.MockReadRequestBuilder import
MockReadRequestBuilder
-
-
-@dataclass
-class MockPlcConnection(PlcConnection):
- _is_connected: bool = False
-
- def connect(self):
- """
- Connect the Mock PLC connection
- :return:
- """
- self._is_connected = True
-
- def is_connected(self) -> bool:
- """
- Return the current status of the Mock PLC Connection
- :return bool: True is connected
- """
- return self._is_connected
-
- def close(self) -> None:
- """
- Closes the connection to the remote PLC.
- :return:
- """
- self._is_connected = False
-
- def read_request_builder(self) -> ReadRequestBuilder:
- """
- :return: read request builder.
- """
- return MockReadRequestBuilder()
-
- def _default_failed_request(
- self, code: PlcResponseCode
- ) -> Awaitable[PlcReadResponse]:
- """
- Returns a default PlcResponse, mainly used in case of a failed request
- :param code: The response code to return
- :return: The PlcResponse
- """
- loop = asyncio.get_running_loop()
- fut = loop.create_future()
- fut.set_result(PlcResponse(code))
- return fut
-
- def execute(self, request: PlcReadRequest) -> Awaitable[PlcReadResponse]:
- """
- Executes a PlcRequest as long as it's already connected
- :param PlcRequest: Plc Request to execute
- :return: The response from the Plc/Device
- """
- if not self.is_connected():
- return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)
-
- return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)
diff --git a/sandbox/plc4py/tests/unit/plc4py/api/test_PlcRequest.py
b/sandbox/plc4py/tests/unit/plc4py/api/test_PlcRequest.py
index 2e15de85c0..027003e8d6 100644
--- a/sandbox/plc4py/tests/unit/plc4py/api/test_PlcRequest.py
+++ b/sandbox/plc4py/tests/unit/plc4py/api/test_PlcRequest.py
@@ -16,6 +16,8 @@
# specific language governing permissions and limitations
# under the License.
#
+from typing import cast
+
import pytest
from plc4py.api.PlcConnection import PlcConnection
@@ -28,7 +30,7 @@ from plc4py.api.value.PlcValue import PlcResponseCode
from plc4py.spi.messages.utils.ResponseItem import ResponseItem
from plc4py.spi.values.PlcBOOL import PlcBOOL
from plc4py.spi.values.PlcINT import PlcINT
-from tests.unit.plc4py.api.test.MockPlcConection import MockPlcConnection
+from plc4py.drivers.mock.MockConnection import MockConnection
def test_read_request_builder_empty_request(mocker) -> None:
@@ -37,7 +39,7 @@ def test_read_request_builder_empty_request(mocker) -> None:
:param mocker:
:return:
"""
- connection: PlcConnection = MockPlcConnection()
+ connection: PlcConnection = MockConnection()
# the connection function is supposed to support context manager
# so using it in a with statement should result in close being called on
the connection
@@ -52,7 +54,7 @@ def test_read_request_builder_non_empty_request(mocker) ->
None:
:param mocker:
:return:
"""
- connection: PlcConnection = MockPlcConnection()
+ connection: PlcConnection = MockConnection()
# the connection function is supposed to support context manager
# so using it in a with statement should result in close being called on
the connection
@@ -72,7 +74,7 @@ async def
test_read_request_builder_non_empty_request_not_connected(mocker) -> N
:param mocker:
:return:
"""
- connection: PlcConnection = MockPlcConnection()
+ connection: PlcConnection = MockConnection()
# the connection function is supposed to support context manager
# so using it in a with statement should result in close being called on
the connection
@@ -85,6 +87,60 @@ async def
test_read_request_builder_non_empty_request_not_connected(mocker) -> N
assert response.code == PlcResponseCode.NOT_CONNECTED
[email protected]
+async def test_read_request_builder_non_empty_request_connected_bool(mocker)
-> None:
+ """
+ Create a request with a field and then confirm an non empty response gets
returned with a OK code
+ :param mocker:
+ :return:
+ """
+ connection: PlcConnection = MockConnection()
+ connection.connect()
+ field = "1:BOOL"
+
+ # the connection function is supposed to support context manager
+ # so using it in a with statement should result in close being called on
the connection
+ with connection.read_request_builder() as builder:
+ builder.add_item(field)
+ request: PlcFieldRequest = builder.build()
+ response: PlcReadResponse = cast(
+ PlcReadResponse, await connection.execute(request)
+ )
+
+ # verify that request has one field
+ assert response.code == PlcResponseCode.OK
+
+ value = response.values[field][0].value
+ assert not value.get_bool()
+
+
[email protected]
+async def test_read_request_builder_non_empty_request_connected_int(mocker) ->
None:
+ """
+ Create a request with a field and then confirm an non empty response gets
returned with a OK code
+ :param mocker:
+ :return:
+ """
+ connection: PlcConnection = MockConnection()
+ connection.connect()
+ field = "1:INT"
+
+ # the connection function is supposed to support context manager
+ # so using it in a with statement should result in close being called on
the connection
+ with connection.read_request_builder() as builder:
+ builder.add_item(field)
+ request: PlcFieldRequest = builder.build()
+ response: PlcReadResponse = cast(
+ PlcReadResponse, await connection.execute(request)
+ )
+
+ # verify that request has one field
+ assert response.code == PlcResponseCode.OK
+
+ value = response.values[field][0].value
+ assert value.get_int() == 0
+
+
def test_read_response_boolean_response(mocker) -> None:
"""
Create a Plc Response with a boolean field, confirm that a boolean gets
returned
diff --git a/sandbox/plc4py/tests/unit/plc4py/test_PlcDriverManager.py
b/sandbox/plc4py/tests/unit/plc4py/test_PlcDriverManager.py
index a18b652f64..cf89c38792 100644
--- a/sandbox/plc4py/tests/unit/plc4py/test_PlcDriverManager.py
+++ b/sandbox/plc4py/tests/unit/plc4py/test_PlcDriverManager.py
@@ -19,7 +19,7 @@
from unittest.mock import MagicMock
from plc4py.PlcDriverManager import PlcDriverManager
-from tests.unit.plc4py.api.test.MockPlcConection import MockPlcConnection
+from plc4py.drivers.mock.MockConnection import MockConnection
def test_connection_context_manager_impl_close_called(mocker) -> None:
@@ -27,7 +27,7 @@ def test_connection_context_manager_impl_close_called(mocker)
-> None:
# getup a plain return value for get_connection
connection_mock: MagicMock = mocker.patch.object(manager, "get_connection")
- connection_mock.return_value = MockPlcConnection()
+ connection_mock.return_value = MockConnection()
# the connection function is supposed to support context manager
# so using it in a with statement should result in close being called on
the connection