diff --git a/enocean/communicators/communicator.py b/enocean/communicators/communicator.py index 9aff7c0..ccec026 100644 --- a/enocean/communicators/communicator.py +++ b/enocean/communicators/communicator.py @@ -1,5 +1,7 @@ # -*- encoding: utf-8 -*- from __future__ import print_function, unicode_literals, division, absolute_import +from enocean.protocol.esp2_packet import ESP2Packet +from enocean.protocol.packet import ESP3Packet import logging import datetime @@ -11,6 +13,11 @@ from enocean.protocol.packet import Packet, UTETeachInPacket from enocean.protocol.constants import PACKET, PARSE_RESULT, RETURN_CODE +from enum import Enum + +class ESP_Version(Enum): + ESP2 = 2 + ESP3 = 3 class Communicator(threading.Thread): ''' @@ -19,7 +26,7 @@ class Communicator(threading.Thread): ''' logger = logging.getLogger('enocean.communicators.Communicator') - def __init__(self, callback=None, teach_in=True): + def __init__(self, version: ESP_Version=ESP_Version.ESP3, callback=None, teach_in=True): super(Communicator, self).__init__() # Create an event to stop the thread self._stop_flag = threading.Event() @@ -35,6 +42,7 @@ def __init__(self, callback=None, teach_in=True): # Should new messages be learned automatically? Defaults to True. # TODO: Not sure if we should use CO_WR_LEARNMODE?? self.teach_in = teach_in + self._version = version def _get_from_send_queue(self): ''' Get message from send queue, if one exists ''' @@ -61,11 +69,15 @@ def parse(self): ''' Parses messages and puts them to receive queue ''' # Loop while we get new messages while True: - status, self._buffer, packet = Packet.parse_msg(self._buffer) + status, self._buffer, packet = ESP3Packet.parse_msg(self._buffer) if self._version == ESP_Version.ESP3.value else ESP2Packet.parse_msg(self._buffer) # If message is incomplete -> break the loop if status == PARSE_RESULT.INCOMPLETE: return status + # if message has a crc error, ignore current buffer + if status == PARSE_RESULT.CRC_MISMATCH: + self._buffer.clear() + # If message is OK, add it to receive queue or send to the callback method if status == PARSE_RESULT.OK and packet: packet.received = datetime.datetime.now() diff --git a/enocean/communicators/serialcommunicator.py b/enocean/communicators/serialcommunicator.py index ffe4201..fc54214 100644 --- a/enocean/communicators/serialcommunicator.py +++ b/enocean/communicators/serialcommunicator.py @@ -4,15 +4,15 @@ import serial import time -from enocean.communicators.communicator import Communicator +from enocean.communicators.communicator import Communicator, ESP_Version class SerialCommunicator(Communicator): ''' Serial port communicator class for EnOcean radio ''' logger = logging.getLogger('enocean.communicators.SerialCommunicator') - def __init__(self, port='/dev/ttyAMA0', callback=None): - super(SerialCommunicator, self).__init__(callback) + def __init__(self, version: ESP_Version=ESP_Version.ESP3, port='/dev/ttyAMA0', callback=None): + super(SerialCommunicator, self).__init__(version = version, callback = callback) # Initialize serial port self.__ser = serial.Serial(port, 57600, timeout=0.1) diff --git a/enocean/communicators/tcpcommunicator.py b/enocean/communicators/tcpcommunicator.py index 239012d..7379c00 100644 --- a/enocean/communicators/tcpcommunicator.py +++ b/enocean/communicators/tcpcommunicator.py @@ -3,15 +3,15 @@ import logging import socket -from enocean.communicators.communicator import Communicator +from enocean.communicators.communicator import Communicator, ESP_Version class TCPCommunicator(Communicator): ''' Socket communicator class for EnOcean radio ''' logger = logging.getLogger('enocean.communicators.TCPCommunicator') - def __init__(self, host='', port=9637): - super(TCPCommunicator, self).__init__() + def __init__(self, version: ESP_Version=ESP_Version.ESP3, host='', port=9637): + super(TCPCommunicator, self).__init__(version = version) self.host = host self.port = port diff --git a/enocean/protocol/constants.py b/enocean/protocol/constants.py index 8dd3e74..f659195 100644 --- a/enocean/protocol/constants.py +++ b/enocean/protocol/constants.py @@ -34,6 +34,25 @@ class RETURN_CODE(IntEnum): WRONG_PARAM = 0x03 OPERATION_DENIED = 0x04 +# ESP2 ORG EnOcean_Equipment_Profiles_2.0.pdf +class ORG(IntEnum): + BS4 = 0x07 + BS1 = 0x06 + RPS = 0x05 + +#ESP2 Message Status Field EnOcean_Equipment_Profiles_2.0.pdf +class MSGSTATUS(IntEnum): + #RPS definitions + T2Msg = 0x20 + NMsg = 0x10 + T2NMsg = 0x30 + T2UMsg = 0x20 + T1NMsg = 0x10 + T1UMsg = 0x00 + #BS4 definitions + BS4 = 0x00 #must be zero in BS4 messages with RP count of 0 + #BS1 definitions + BS1 = 0x00 #must be zero in BS1 too # EnOceanSerialProtocol3.pdf / 20 class EVENT_CODE(IntEnum): diff --git a/enocean/protocol/crc8.py b/enocean/protocol/crc8.py index 5dd9b9d..8925d6c 100644 --- a/enocean/protocol/crc8.py +++ b/enocean/protocol/crc8.py @@ -34,8 +34,15 @@ 0xfa, 0xfd, 0xf4, 0xf3) -def calc(msg): +def calc_ESP3(msg): checksum = 0 for byte in msg: checksum = CRC_TABLE[checksum & 0xFF ^ byte & 0xFF] return checksum + +def calc_ESP2(msg): + checksum=0 + for byte in msg: + checksum += byte + checksum &= 0xFF + return checksum \ No newline at end of file diff --git a/enocean/protocol/esp2_packet.py b/enocean/protocol/esp2_packet.py new file mode 100644 index 0000000..7165307 --- /dev/null +++ b/enocean/protocol/esp2_packet.py @@ -0,0 +1,218 @@ +# -*- encoding: utf-8 -*- +from __future__ import print_function, unicode_literals, division, absolute_import +from enocean.protocol.packet import EventPacket, Packet, RadioPacket, ResponsePacket, UTETeachInPacket + +from enocean.protocol import crc8 +from enocean.protocol.eep import EEP +from enocean.protocol.constants import ORG, PACKET, RORG, PARSE_RESULT + + +class ESP2Packet(Packet): + ''' + Base class for ESP2 Packet. + Mainly used for for packet generation and + Packet.parse_msg(buf) for parsing message. + parse_msg() returns subclass, if one is defined for the data type. + ''' + + @staticmethod + def parse_msg(buf): + ''' + Parses message from buffer. + returns: + - PARSE_RESULT + - remaining buffer + - Packet -object (if message was valid, else None) + ''' + # If the buffer doesn't contain 0xA5 (start char) + # the message isn't needed -> ignore + # if 0xA5 not in buf: + # return PARSE_RESULT.INCOMPLETE, [], None + # else: + # if buf[list(buf).index(0xA5)+1] != 0x5A: + # return PARSE_RESULT.INCOMPLETE, [], None + + if not buf: + return PARSE_RESULT.INCOMPLETE, [], None + + if 0xA5 not in buf: + return PARSE_RESULT.INCOMPLETE, [], None + + msg_len = 0 + + for index,value in enumerate(buf): + try: + if buf[index] == 0xA5 and buf[index+1] == 0x5A: + data_len = buf[index+2] & 0x1F + HSEQ = buf[index+2] >> 5 + opt_len = 0 + msg_len = data_len + if len(buf[index+2:]) < msg_len: + # If buffer isn't long enough, the message is incomplete + return PARSE_RESULT.INCOMPLETE, buf, None + crcval = crc8.calc_ESP2(buf[index+2:index+2+data_len]) + if buf[index+2+data_len] == crcval: + buf = buf[index+2:] + break + else: + ESP2Packet.logger.error('Data CRC error!') + return PARSE_RESULT.CRC_MISMATCH, buf, None + except IndexError: + # If the fields don't exist, message is incomplete + return PARSE_RESULT.INCOMPLETE, buf, None + + if msg_len == 0: + ESP2Packet.logger.error('Data Length is Zero!') + return PARSE_RESULT.INCOMPLETE, [], None + + msg = buf[0:msg_len] + buf = buf[msg_len+1:] + + packet_type = HSEQ + data = msg[1:data_len] + opt_data = [] + + #Adopt ORG to RORG for ESP2 + if data[0] == ORG.BS1: + data[0] = RORG.BS1 + if data[0] == ORG.BS4: + data[0] = RORG.BS4 + if data[0] == ORG.RPS: + data[0] = RORG.RPS + + # If we got this far, everything went ok (?) + # in ESP2 the exist only two type of packets, RadioPacket and Command + if packet_type == PACKET.RADIO or packet_type == PACKET.RESERVED or packet_type == PACKET.EVENT: + # Need to handle UTE Teach-in here, as it's a separate packet type... + if data[0] == RORG.UTE: + packet = ESP2UTETeachInPacket(packet_type, data, opt_data) + # Send a response automatically, works only if + # - communicator is set + # - communicator.teach_in == True + # packet.send_response() + else: + packet = ESP2RadioPacket(packet_type, data, opt_data) + elif packet_type == PACKET.RESPONSE: + packet = ESP2ResponsePacket(packet_type, data, opt_data) + else: + packet = ESP2Packet(packet_type, data, opt_data) + + return PARSE_RESULT.OK, buf, packet + + @staticmethod + def create(packet_type, rorg, rorg_func, rorg_type, direction=None, command=None, + destination=None, + sender=None, + learn=False, **kwargs): + ''' + Creates an packet ready for sending. + Uses rorg, rorg_func and rorg_type to determine the values set based on EEP. + Additional arguments (**kwargs) are used for setting the values. + Currently only supports: + - PACKET.RADIO + - RORGs RPS, BS1, BS4, VLD. + TODO: + - Require sender to be set? Would force the "correct" sender to be set. + - Do we need to set telegram control bits? + Might be useful for acting as a repeater? + ''' + + if packet_type != PACKET.RADIO: + # At least for now, only support PACKET.RADIO. + raise ValueError('Packet type not supported by this function.') + + if rorg not in [RORG.RPS, RORG.BS1, RORG.BS4]: + # At least for now, only support these RORGS. + raise ValueError('RORG not supported by this function.') + + if destination is None: + destination = [0xFF, 0xFF, 0xFF, 0xFF] + + # TODO: Should use the correct Base ID as default. + # Might want to change the sender to be an offset from the actual address? + if sender is None: + ESP2Packet.logger.warning('Replacing sender with default address.') + sender = [0xDE, 0xAD, 0xBE, 0xEF] + + if not isinstance(destination, list) or len(destination) != 4: + raise ValueError('Destination must a list containing 4 (numeric) values.') + + if not isinstance(sender, list) or len(sender) != 4: + raise ValueError('Sender must a list containing 4 (numeric) values.') + + packet = ESP2Packet(packet_type, data=[], optional=[]) + packet.rorg = rorg + + if packet.rorg == RORG.BS1: + packet.rorg = ORG.BS1 + if packet.rorg == RORG.BS4: + packet.rorg = ORG.BS4 + if packet.rorg == RORG.RPS: + packet.rorg = ORG.RPS + + packet.data = [packet.rorg] + # Select EEP at this point, so we know how many bits we're dealing with (for VLD). + #packet.select_eep(rorg_func, rorg_type, direction, command) + + # Initialize data depending on the profile. + # if rorg in [RORG.RPS, RORG.BS1]: + # packet.data.extend([0]) + #elif rorg == RORG.BS4: + packet.data.extend(command) + #else: + # packet.data.extend([0] * int(packet._profile.get('bits', '1'))) + packet.data.extend(sender) + packet.data.extend([0]) + # Always use sub-telegram 3, maximum dbm (as per spec, when sending), + # and no security (security not supported as per EnOcean Serial Protocol). + #packet.optional = [3] + destination + [0xFF] + [0] + + #if command: + # Set CMD to command, if applicable.. Helps with VLD. + #kwargs['CMD'] = command + + #packet.set_eep(kwargs) + if rorg in [RORG.BS1, RORG.BS4] and not learn: + if rorg == RORG.BS1: + packet.data[1] |= (1 << 3) + if rorg == RORG.BS4: + packet.data[4] |= (1 << 3) + packet.data[-1] = packet.status + + # Parse the built packet, so it corresponds to the received packages + # For example, stuff like RadioPacket.learn should be set. + packet = ESP2Packet.parse_msg(packet.build())[2] + return packet + + def build(self): + ''' Build Packet for sending to EnOcean controller ''' + data_length = len(self.data)+1 + ords = [0xA5, 0x5A, (data_length & 0x1F | ((int(self.packet_type)&0x07)<<5))] + + if self.data[0] in [RORG.RPS, RORG.BS4, RORG.BS1]: + if self.data[0] == RORG.RPS: + self.data[0] = ORG.RPS + if self.data[0] == RORG.BS1: + self.data[0] = ORG.BS1 + if self.data[0] == RORG.BS4: + self.data[0] = ORG.BS4 + + ords.extend(self.data) + ords.append(crc8.calc_ESP2(ords[2:])) + return ords + +class ESP2RadioPacket(RadioPacket): + @staticmethod + def create(rorg, rorg_func, rorg_type, direction=None, command=None, + destination=None, sender=None, learn=False, **kwargs): + return ESP2Packet.create(PACKET.RADIO_ERP1, rorg, rorg_func, rorg_type, + direction, command, destination, sender, learn, **kwargs) + +class ESP2ResponsePacket(ResponsePacket, ESP2Packet): + """ESP2 version of response package""" + +class ESP2EventPacket(EventPacket, ESP2Packet): + """ESP2 version of event package""" + +class ESP2UTETeachInPacket(UTETeachInPacket, ESP2RadioPacket): + """ESP2 version of UTE teachin package""" \ No newline at end of file diff --git a/enocean/protocol/packet.py b/enocean/protocol/packet.py index 14fdc34..bf0f05c 100644 --- a/enocean/protocol/packet.py +++ b/enocean/protocol/packet.py @@ -6,7 +6,7 @@ import enocean.utils from enocean.protocol import crc8 from enocean.protocol.eep import EEP -from enocean.protocol.constants import PACKET, RORG, PARSE_RESULT, DB0, DB2, DB3, DB4, DB6 +from enocean.protocol.constants import PACKET, PARSE_RESULT, RORG, DB0, DB2, DB3, DB4, DB6 class Packet(object): @@ -137,14 +137,14 @@ def parse_msg(buf): opt_data = msg[6 + data_len:6 + data_len + opt_len] # Check CRCs for header and data - if msg[5] != crc8.calc(msg[1:5]): + if msg[5] != crc8.calc_ESP3(msg[1:5]): # Fail if doesn't match message - Packet.logger.error('Header CRC error!') + ESP3Packet.logger.error('Header CRC error!') # Return CRC_MISMATCH return PARSE_RESULT.CRC_MISMATCH, buf, None - if msg[6 + data_len + opt_len] != crc8.calc(msg[6:6 + data_len + opt_len]): + if msg[6 + data_len + opt_len] != crc8.calc_ESP3(msg[6:6 + data_len + opt_len]): # Fail if doesn't match message - Packet.logger.error('Data CRC error!') + ESP3Packet.logger.error('Data CRC error!') # Return CRC_MISMATCH return PARSE_RESULT.CRC_MISMATCH, buf, None @@ -152,15 +152,15 @@ def parse_msg(buf): if packet_type == PACKET.RADIO_ERP1: # Need to handle UTE Teach-in here, as it's a separate packet type... if data[0] == RORG.UTE: - packet = UTETeachInPacket(packet_type, data, opt_data) + packet = ESP3UTETeachInPacket(packet_type, data, opt_data) else: - packet = RadioPacket(packet_type, data, opt_data) + packet = ESP3RadioPacket(packet_type, data, opt_data) elif packet_type == PACKET.RESPONSE: - packet = ResponsePacket(packet_type, data, opt_data) + packet = ESP3ResponsePacket(packet_type, data, opt_data) elif packet_type == PACKET.EVENT: - packet = EventPacket(packet_type, data, opt_data) + packet = ESP3EventPacket(packet_type, data, opt_data) else: - packet = Packet(packet_type, data, opt_data) + packet = ESP3Packet(packet_type, data, opt_data) return PARSE_RESULT.OK, buf, packet @@ -186,20 +186,20 @@ def create(packet_type, rorg, rorg_func, rorg_type, direction=None, command=None if packet_type != PACKET.RADIO_ERP1: # At least for now, only support PACKET.RADIO_ERP1. - raise ValueError('Packet type not supported by this function.') + raise ValueError('ESP3 Packet type not supported by this function.') if rorg not in [RORG.RPS, RORG.BS1, RORG.BS4, RORG.VLD]: # At least for now, only support these RORGS. raise ValueError('RORG not supported by this function.') if destination is None: - Packet.logger.warning('Replacing destination with broadcast address.') + ESP3Packet.logger.warning('Replacing destination with broadcast address.') destination = [0xFF, 0xFF, 0xFF, 0xFF] # TODO: Should use the correct Base ID as default. # Might want to change the sender to be an offset from the actual address? if sender is None: - Packet.logger.warning('Replacing sender with default address.') + ESP3Packet.logger.warning('Replacing sender with default address.') sender = [0xDE, 0xAD, 0xBE, 0xEF] if not isinstance(destination, list) or len(destination) != 4: @@ -208,7 +208,7 @@ def create(packet_type, rorg, rorg_func, rorg_type, direction=None, command=None if not isinstance(sender, list) or len(sender) != 4: raise ValueError('Sender must a list containing 4 (numeric) values.') - packet = Packet(packet_type, data=[], optional=[]) + packet = ESP3Packet(packet_type, data=[], optional=[]) packet.rorg = rorg packet.data = [packet.rorg] # Select EEP at this point, so we know how many bits we're dealing with (for VLD). @@ -241,7 +241,7 @@ def create(packet_type, rorg, rorg_func, rorg_type, direction=None, command=None # Parse the built packet, so it corresponds to the received packages # For example, stuff like RadioPacket.learn should be set. - packet = Packet.parse_msg(packet.build())[2] + packet = ESP3Packet.parse_msg(packet.build())[2] packet.rorg = rorg packet.parse_eep(rorg_func, rorg_type, direction, command) return packet @@ -285,13 +285,12 @@ def build(self): ''' Build Packet for sending to EnOcean controller ''' data_length = len(self.data) ords = [0x55, (data_length >> 8) & 0xFF, data_length & 0xFF, len(self.optional), int(self.packet_type)] - ords.append(crc8.calc(ords[1:5])) + ords.append(crc8.calc_ESP3(ords[1:5])) ords.extend(self.data) ords.extend(self.optional) - ords.append(crc8.calc(ords[6:])) + ords.append(crc8.calc_ESP3(ords[6:])) return ords - class RadioPacket(Packet): destination = [0xFF, 0xFF, 0xFF, 0xFF] dBm = 0 @@ -325,9 +324,12 @@ def destination_int(self): def destination_hex(self): return enocean.utils.to_hex_string(self.destination) + def instance(self): RadioPacket + def parse(self): - self.destination = self.optional[1:5] - self.dBm = -self.optional[5] + if len(self.optional): + self.destination = self.optional[1:5] + self.dBm = -self.optional[5] self.sender = self.data[-5:-1] # Default to learn == True, as some devices don't have a learn button self.learn = True @@ -433,3 +435,54 @@ def parse(self): self.event = self.data[0] self.event_data = self.data[1:] return super(EventPacket, self).parse() + + + + +class ESP3Packet(Packet): + ''' + Base class for ESP3 Packet. + Mainly used for for packet generation and + Packet.parse_msg(buf) for parsing message. + parse_msg() returns subclass, if one is defined for the data type. + ''' + + @staticmethod + def parse_msg(buf): + ''' + Parses ESP3 message from buffer. + returns: + - PARSE_RESULT + - remaining buffer + - Packet -object (if message was valid, else None) + ''' + return Packet.parse_msg(buf) + + @staticmethod + def create(packet_type, rorg, rorg_func, rorg_type, direction=None, command=None, + destination=None, + sender=None, + learn=False, **kwargs): + ''' + Creates an ESP3 packet ready for sending. + Uses rorg, rorg_func and rorg_type to determine the values set based on EEP. + Additional arguments (**kwargs) are used for setting the values. + ''' + return Packet.create(packet_type, rorg, rorg_func, rorg_type, direction, command, destination, sender, learn, **kwargs) + + +class ESP3RadioPacket(RadioPacket, ESP3Packet): + @staticmethod + def create(rorg, rorg_func, rorg_type, direction=None, command=None, + destination=None, sender=None, learn=False, **kwargs): + return RadioPacket.create(rorg, rorg_func, rorg_type, + direction, command, destination, sender, learn, **kwargs) + +class ESP3ResponsePacket(ResponsePacket, ESP3Packet): + """ESP3 version of response package""" + +class ESP3EventPacket(EventPacket, ESP3Packet): + """ESP3 version of event package""" + +class ESP3UTETeachInPacket(UTETeachInPacket, ESP3RadioPacket): + """ESP3 version of UTE teachin package""" \ No newline at end of file diff --git a/enocean/protocol/tests/test_eep.py b/enocean/protocol/tests/test_eep.py index acdecdf..b9ab2a8 100644 --- a/enocean/protocol/tests/test_eep.py +++ b/enocean/protocol/tests/test_eep.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- from __future__ import print_function, unicode_literals, division, absolute_import +from enocean.protocol.packet import Packet from enocean.protocol.packet import Packet from enocean.protocol.eep import EEP diff --git a/enocean/protocol/tests/test_packet.py b/enocean/protocol/tests/test_packet.py index 96fa4c3..3b342e8 100644 --- a/enocean/protocol/tests/test_packet.py +++ b/enocean/protocol/tests/test_packet.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- from __future__ import print_function, unicode_literals, division, absolute_import +from enocean.protocol.packet import Packet from enocean.protocol.packet import Packet, EventPacket from enocean.protocol.constants import PACKET, PARSE_RESULT, EVENT_CODE diff --git a/enocean/protocol/tests/test_packet_creation.py b/enocean/protocol/tests/test_packet_creation.py index 03a6a8b..9a6cabd 100644 --- a/enocean/protocol/tests/test_packet_creation.py +++ b/enocean/protocol/tests/test_packet_creation.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- from __future__ import print_function, unicode_literals, division, absolute_import +from enocean.protocol.packet import Packet, RadioPacket from nose.tools import raises from enocean.protocol.packet import Packet, RadioPacket diff --git a/enocean/protocol/tests/test_teachin.py b/enocean/protocol/tests/test_teachin.py index d908a94..c990320 100644 --- a/enocean/protocol/tests/test_teachin.py +++ b/enocean/protocol/tests/test_teachin.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- from __future__ import print_function, unicode_literals, division, absolute_import +from enocean.protocol.packet import Packet from enocean.communicators import Communicator from enocean.protocol.packet import Packet diff --git a/examples/enocean_example.py b/examples/enocean_example.py index be9eb0c..2e1d051 100755 --- a/examples/enocean_example.py +++ b/examples/enocean_example.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- +from enocean.protocol.packet import RadioPacket from enocean.consolelogger import init_logging import enocean.utils from enocean.communicators.serialcommunicator import SerialCommunicator diff --git a/examples/example_D2-05-00.py b/examples/example_D2-05-00.py index 20f655d..0427546 100644 --- a/examples/example_D2-05-00.py +++ b/examples/example_D2-05-00.py @@ -7,6 +7,7 @@ Waits for UTE Teach-ins, sends the response automatically and prints the ID of new device. ''' +from enocean.protocol.packet import RadioPacket import sys import time import traceback diff --git a/examples/example_DO21-11B-E.py b/examples/example_DO21-11B-E.py index 52464ec..1835a6c 100644 --- a/examples/example_DO21-11B-E.py +++ b/examples/example_DO21-11B-E.py @@ -7,6 +7,7 @@ Waits for UTE Teach-ins, sends the response automatically and prints the ID of new device. ''' +from enocean.protocol.packet import RadioPacket import sys import time import traceback diff --git a/setup.py b/setup.py index 3fea09c..2af5f69 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='enocean', - version='0.60.1', + version='0.60.2', description='EnOcean serial protocol implementation', author='Kimmo Huoman', author_email='kipenroskaposti@gmail.com',