Skip to content

Commit

Permalink
Separate coil configuration from data #83 (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
yozik04 authored Feb 25, 2023
2 parents 1a4fd44 + ff52eec commit b3165e2
Show file tree
Hide file tree
Showing 18 changed files with 982 additions and 680 deletions.
14 changes: 7 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.4.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
Expand All @@ -16,37 +16,37 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/asottile/pyupgrade
rev: v2.31.1
rev: v3.3.1
hooks:
- id: pyupgrade
args: ["--py37-plus"]

- repo: https://github.com/psf/black
rev: 22.3.0
rev: 23.1.0
hooks:
- id: black
args:
- --safe
- --quiet

- repo: https://github.com/pycqa/isort
rev: 5.10.1
rev: 5.12.0
hooks:
- id: isort

- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies: [flake8-bugbear]

- repo: https://github.com/codespell-project/codespell
rev: v2.1.0
rev: v2.2.2
hooks:
- id: codespell

- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.9.0
rev: v1.10.0
hooks:
- id: python-check-blanket-noqa
- id: python-check-blanket-type-ignore
Expand Down
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ Ports are configurable
import asyncio
import logging

from nibe.coil import Coil
from nibe.coil import CoilData
from nibe.connection.nibegw import NibeGW
from nibe.heatpump import HeatPump, Model

logger = logging.getLogger("nibe").getChild(__name__)

def on_coil_update(coil: Coil):
logger.debug(f"{coil.name}: {coil.value}")
def on_coil_update(coil_data: CoilData):
logger.debug(coil_data)

async def main():
heatpump = HeatPump(Model.F1255)
Expand All @@ -80,14 +80,14 @@ With S series heatpumps
import asyncio
import logging

from nibe.coil import Coil
from nibe.coil import CoilData
from nibe.connection.modbus import Modbus
from nibe.heatpump import HeatPump, Model

logger = logging.getLogger("nibe").getChild(__name__)

def on_coil_update(coil: Coil):
logger.debug(f"on_coil_update: {coil.name}: {coil.value}")
def on_coil_update(coil_data: CoilData):
logger.debug(f"on_coil_update: {coil_data}")

async def main():
heatpump = HeatPump(Model.F1255)
Expand All @@ -99,9 +99,9 @@ async def main():
connection = Modbus(heatpump=heatpump, url="tcp://192.168.1.2:502", slave_id=1)

coil = heatpump.get_coil_by_name('bt50-room-temp-s1-40033')
await connection.read_coil(coil)
coil_data = await connection.read_coil(coil)

logger.debug(f"main: {coil.name}: {coil.value}")
logger.debug(f"main: {coil_data}")

if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
Expand All @@ -118,14 +118,14 @@ With NIBE MODBUS 40
import asyncio
import logging

from nibe.coil import Coil
from nibe.coil import CoilData
from nibe.connection.modbus import Modbus
from nibe.heatpump import HeatPump, Model

logger = logging.getLogger("nibe").getChild(__name__)

def on_coil_update(coil: Coil):
logger.debug(f"on_coil_update: {coil.name}: {coil.value}")
def on_coil_update(coil_data: CoilData):
logger.debug(f"on_coil_update: {coil_data}")

async def main():
heatpump = HeatPump(Model.F1255)
Expand All @@ -137,9 +137,9 @@ async def main():
connection = Modbus(heatpump=heatpump, url="serial:///dev/ttyS0", slave_id=1, conn_options={"baudrate": 9600})

coil = heatpump.get_coil_by_name('bt50-room-temp-s1-40033')
await connection.read_coil(coil)
coil_data = await connection.read_coil(coil)

logger.debug(f"main: {coil.name}: {coil.value}")
logger.debug(f"main: {coil_data}")

if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
Expand Down
2 changes: 1 addition & 1 deletion nibe/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.6.0"
__version__ = "2.0.0"
168 changes: 110 additions & 58 deletions nibe/coil.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dataclasses import dataclass
from typing import Dict, Optional, Union

from nibe.exceptions import NoMappingException
from nibe.exceptions import NoMappingException, ValidationError


def is_coil_boolean(coil):
Expand All @@ -17,9 +18,10 @@ def is_coil_boolean(coil):


class Coil:
"""Represents a coil."""

mappings: Optional[Dict[str, str]]
reverse_mappings: Optional[Dict[str, str]]
_value: Union[int, float, str, None]

def __init__(
self,
Expand Down Expand Up @@ -67,52 +69,24 @@ def __init__(
if self.is_boolean and not mappings:
self.set_mappings({"0": "OFF", "1": "ON"})

self._value = None

def set_mappings(self, mappings):
"""Set mappings for value translation."""
if mappings:
self.mappings = {k: v.upper() for k, v in mappings.items()}
self.reverse_mappings = {v.upper(): k for k, v in mappings.items()}
else:
self.mappings = None
self.reverse_mappings = None

@property
def value(self) -> Union[int, float, str, None]:
return self._value

@value.setter
def value(self, value: Union[int, float, str, None]):
if value is None:
self._value = None
return

if self.reverse_mappings:
assert isinstance(
value, str
), f"Provided value '{value}' is invalid type (str is supported) for {self.name}"

value = value.upper()
assert (
value in self.reverse_mappings
), f"Provided value '{value}' is not in {self.reverse_mappings.keys()} for {self.name}"

self._value = value
return

assert isinstance(
value, (int, float)
), f"Provided value '{value}' is invalid type (int and float are supported) for {self.name}"

self.check_value_bounds(value)

self._value = value

@property
def has_mappings(self):
"""Return True if mappings are defined."""
return self.mappings is not None

def get_mapping_for(self, value: int):
"""Return mapping for value.
:raises NoMappingException: When no mapping is found"""
if not self.mappings:
raise NoMappingException(f"No mappings defined for {self.name}")

Expand All @@ -123,38 +97,116 @@ def get_mapping_for(self, value: int):
f"Mapping not found for {self.name} coil for value: {value}"
)

def get_reverse_mapping_for(self, value: Union[int, float, str, None]):
def get_reverse_mapping_for(self, value: Union[int, float, str, None]) -> int:
"""Return reverse mapping for value.
:raises NoMappingException: When no mapping is found"""
if not isinstance(value, str):
raise ValidationError(
f"{self.name} coil value ({value}) is invalid type (str is expected)"
)

if not self.reverse_mappings:
raise NoMappingException(f"No reverse mappings defined for {self.name}")
raise NoMappingException(
f"{self.name} coil has no reverse mappings defined"
)

try:
return self.reverse_mappings[str(value)]
value = value.upper()
return int(self.reverse_mappings[str(value)])
except KeyError:
raise NoMappingException(
f"Reverse mapping not found for {self.name} coil for value: {value}"
f"{self.name} coil reverse mapping not found for value: {value}"
)

def check_value_bounds(self, value):
if self.min is not None:
assert (
value >= self.min
), f"{self.name} coil value ({value}) is smaller than min allowed ({self.min})"
def is_raw_value_valid(self, value: int) -> bool:
"""Return True if provided raw value is valid."""
if not isinstance(value, int):
return False

if self.max is not None:
assert (
value <= self.max
), f"{self.name} coil value ({value}) is larger than max allowed ({self.max})"
if self.raw_min is not None and value < self.raw_min:
return False

def check_raw_value_bounds(self, value):
if self.raw_min is not None:
assert (
value >= self.raw_min
), f"value ({value}) is smaller than min allowed ({self.raw_min})"
if self.raw_max is not None and value > self.raw_max:
return False

if self.raw_max is not None:
assert (
value <= self.raw_max
), f"value ({value}) is larger than max allowed ({self.raw_max})"
return True

def __repr__(self):
return f"Coil {self.address}, name: {self.name}, title: {self.title}, value: {self.value}"
return f"Coil {self.address}, name: {self.name}, title: {self.title}"


@dataclass
class CoilData:
"""Represents a coil data."""

coil: Coil
value: Union[int, float, str, None] = None

def __repr__(self) -> str:
return f"Coil {self.coil.name}, value: {self.value}"

@staticmethod
def from_mapping(coil: Coil, value: int) -> "CoilData":
"""Create CoilData from raw value using mappings."""
return CoilData(coil, coil.get_mapping_for(value))

@staticmethod
def from_raw_value(coil: Coil, value: int) -> "CoilData":
"""Create CoilData from raw value."""
assert coil.is_raw_value_valid(
value
), f"Raw value {value} is out of range for coil {coil.name}"

if coil.has_mappings:
return CoilData.from_mapping(coil, value)

return CoilData(coil, value / coil.factor)

@property
def raw_value(self) -> int:
"""Return raw value for coil."""
if self.coil.has_mappings:
return self.coil.get_reverse_mapping_for(self.value)

assert isinstance(
self.value, (int, float)
), f"Provided value '{self.value}' is invalid type (int or float is supported) for {self.coil.name}"

raw_value = int(self.value * self.coil.factor)
assert self.coil.is_raw_value_valid(
raw_value
), f"Value {self.value} is out of range for coil {self.coil.name}"

return raw_value

def validate(self) -> None:
"""Validate coil data.
:raises ValidationError: when validation fails"""
if self.value is None:
raise ValidationError(f"Value for {self.coil.name} is not set")

if self.coil.has_mappings:
self.coil.get_reverse_mapping_for(
self.value
) # can throw NoMappingException(ValidationException) or AssertionError
return

if not isinstance(self.value, (int, float)):
raise ValidationError(
f"{self.coil.name} coil value ({self.value}) is invalid type (expected int or float)"
)

self._check_value_bounds()

def _check_value_bounds(self):
if self.coil.min is not None and self.value < self.coil.min:
raise ValidationError(
f"{self.coil.name} coil value ({self.value}) is smaller than min allowed ({self.coil.min})"
)

if self.coil.max is not None and self.value > self.coil.max:
raise ValidationError(
f"{self.coil.name} coil value ({self.value}) is larger than max allowed ({self.coil.max})"
)
Loading

0 comments on commit b3165e2

Please sign in to comment.