"""
Component to create an interface to the Loxone Miniserver.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/loxone/
"""
import asyncio
import binascii
import datetime
import hashlib
import json
import logging
import os
import time
import traceback
import urllib.request as req
import uuid
from base64 import b64encode
from datetime import datetime
from struct import unpack
import queue
import requests_async as requests

import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.helpers.entity import Entity
from homeassistant.config import get_default_config_dir
from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_PORT,
                                 CONF_USERNAME, EVENT_COMPONENT_LOADED,
                                 EVENT_HOMEASSISTANT_START,
                                 EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers.discovery import async_load_platform
from requests.auth import HTTPBasicAuth

REQUIREMENTS = ['websockets', "pycryptodome", "numpy", "requests_async"]

# Loxone constants
TIMEOUT = 10
KEEP_ALIVE_PERIOD = 240

IV_BYTES = 16
AES_KEY_SIZE = 32

SALT_BYTES = 16
SALT_MAX_AGE_SECONDS = 60 * 60
SALT_MAX_USE_COUNT = 30

TOKEN_PERMISSION = 4  # 2=web, 4=app
TOKEN_REFRESH_RETRY_COUNT = 5
# token will be refreshed 1 day before its expiration date
TOKEN_REFRESH_SECONDS_BEFORE_EXPIRY = 24 * 60 * 60  # 1 day
#  if can't determine token expiration date, it will be refreshed after 2 days
TOKEN_REFRESH_DEFAULT_SECONDS = 2 * 24 * 60 * 60  # 2 days

CMD_GET_PUBLIC_KEY = "jdev/sys/getPublicKey"
CMD_KEY_EXCHANGE = "jdev/sys/keyexchange/"
CMD_GET_KEY_AND_SALT = "jdev/sys/getkey2/"
CMD_REQUEST_TOKEN = "jdev/sys/gettoken/"
CMD_REQUEST_TOKEN_JSON_WEB = "jdev/sys/getjwt/"
CMD_GET_KEY = "jdev/sys/getkey"
CMD_AUTH_WITH_TOKEN = "authwithtoken/"
CMD_REFRESH_TOKEN = "jdev/sys/refreshtoken/"
CMD_REFRESH_TOKEN_JSON_WEB = "jdev/sys/refreshjwt/"
CMD_ENCRYPT_CMD = "jdev/sys/enc/"
CMD_ENABLE_UPDATES = "jdev/sps/enablebinstatusupdate"
CMD_GET_VISUAL_PASSWD = "jdev/sys/getvisusalt/"

DEFAULT_TOKEN_PERSIST_NAME = "lox_token.cfg"
ERROR_VALUE = -1
# End of loxone constants

_LOGGER = logging.getLogger(__name__)

DEFAULT_PORT = 8080
EVENT = 'loxone_event'
DOMAIN = 'loxone'
SENDDOMAIN = "loxone_send"
SECUREDSENDDOMAIN = "loxone_send_secured"
DEFAULT = ""
ATTR_UUID = 'uuid'
ATTR_VALUE = 'value'
ATTR_CODE = "code"
ATTR_COMMAND = "command"
CONF_SCENE_GEN = "generate_scenes"

LOXONE_PLATFORMS = ["sensor", "switch", "cover", "light", "scene", "alarm_control_panel"]

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        vol.Required(CONF_USERNAME): cv.string,
        vol.Required(CONF_PASSWORD): cv.string,
        vol.Required(CONF_HOST): cv.string,
        vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
        vol.Optional(CONF_SCENE_GEN, default=True): cv.boolean,
    }),
}, extra=vol.ALLOW_EXTRA)


class loxApp(object):
    def __init__(self):
        self.host = None
        self.port = None
        self.loxapppath = "/data/LoxAPP3.json"

        self.lox_user = None
        self.lox_pass = None
        self.json = None
        self.responsecode = None
        self.version = None

    async def getJson (self):
        url_version = "http://{}:{}/jdev/cfg/version".format(self.host, self.port)
        version_resp = await requests.get(url_version,
                                          auth=HTTPBasicAuth(self.lox_user, self.lox_pass),
                                          verify=False, timeout=TIMEOUT)

        if version_resp.status_code == 200:
            vjson = version_resp.json()
            if 'LL' in vjson:
                if 'Code' in vjson['LL'] and 'value' in vjson['LL']:
                    self.version = [int(x) for x in vjson['LL']['value'].split(".")]

        url = "http://" + str(self.host) + ":" + str(self.port) + self.loxapppath
        my_response = await requests.get(url, auth=HTTPBasicAuth(self.lox_user, self.lox_pass),
                                         verify=False, timeout=TIMEOUT)
        if my_response.status_code == 200:
            self.json = my_response.json()
            if self.version is not None:
                self.json['softwareVersion'] = self.version
        else:
            self.json = None
        self.responsecode = my_response.status_code
        return self.responsecode


def get_room_name_from_room_uuid(lox_config, room_uuid):
    if "rooms" in lox_config:
        if room_uuid in lox_config['rooms']:
            return lox_config['rooms'][room_uuid]['name']

    return ""


def get_cat_name_from_cat_uuid(lox_config, cat_uuid):
    if "cats" in lox_config:
        if cat_uuid in lox_config['cats']:
            return lox_config['cats'][cat_uuid]['name']
    return ""


def get_all_switch_entities(json_data):
    return get_all(json_data, ["Pushbutton", "Switch", "TimedSwitch", "Intercom", "LeftRightAnalog", 'UpDownAnalog', 'Slider'])


def get_all_covers(json_data):
    return get_all(json_data, ["Jalousie", "Gate", 'Window'])


def get_all_analog_info(json_data):
    return get_all(json_data, 'InfoOnlyAnalog')


def get_all_digital_info(json_data):
    return get_all(json_data, 'InfoOnlyDigital')


def get_all_light_controller(json_data):
    return get_all(json_data, 'LightControllerV2')


def get_all_alarm(json_data):
    return get_all(json_data, 'Alarm')


def get_all_dimmer(json_data):
    return get_all(json_data, 'Dimmer')


def get_all(json_data, name):
    controls = []
    if isinstance(name, list):
        for c in json_data['controls'].keys():
            if json_data['controls'][c]['type'] in name:
                controls.append(json_data['controls'][c])
    else:
        for c in json_data['controls'].keys():
            if json_data['controls'][c]['type'] == name:
                controls.append(json_data['controls'][c])
    return controls


async def async_setup(hass, config):
    """setup loxone"""

    try:
        lox_config = loxApp()
        lox_config.lox_user = config[DOMAIN][CONF_USERNAME]
        lox_config.lox_pass = config[DOMAIN][CONF_PASSWORD]
        lox_config.host = config[DOMAIN][CONF_HOST]
        lox_config.port = config[DOMAIN][CONF_PORT]
        request_code = await lox_config.getJson()

        if request_code == 200 or request_code == "200":
            hass.data[DOMAIN] = config[DOMAIN]
            hass.data[DOMAIN]['loxconfig'] = lox_config.json
            for platform in LOXONE_PLATFORMS:
                _LOGGER.debug("starting loxone {}...".format(platform))
                hass.async_create_task(
                    async_load_platform(hass, platform, DOMAIN, {}, config)
                )
            del lox_config
        else:
            _LOGGER.error("unable to connect to Loxone")
    except ConnectionError:
        _LOGGER.error("unable to connect to Loxone")
        return False

    lox = LoxWs(user=config[DOMAIN][CONF_USERNAME],
                password=config[DOMAIN][CONF_PASSWORD],
                host=config[DOMAIN][CONF_HOST],
                port=config[DOMAIN][CONF_PORT],
                loxconfig=config[DOMAIN]['loxconfig'])

    async def message_callback(message):
        hass.bus.async_fire(EVENT, message)

    async def start_loxone(event):
        await lox.start()

    async def stop_loxone(event):
        _ = await lox.stop()
        _LOGGER.debug(_)

    async def loxone_discovered(event):
        if "component" in event.data:
            if event.data['component'] == DOMAIN:
                try:
                    _LOGGER.info("loxone discovered")
                    await asyncio.sleep(0.1)
                    # await asyncio.sleep(0)
                    entity_ids = hass.states.async_all()
                    sensors_analog = []
                    sensors_digital = []
                    switches = []
                    covers = []
                    lights = []

                    for s in entity_ids:
                        s_dict = s.as_dict()
                        attr = s_dict['attributes']
                        if "plattform" in attr and \
                                attr['plattform'] == DOMAIN:
                            if attr['device_typ'] == "analog_sensor":
                                sensors_analog.append(s_dict['entity_id'])
                            elif attr['device_typ'] == "digital_sensor":
                                sensors_digital.append(s_dict['entity_id'])
                            elif attr['device_typ'] == "Jalousie" or \
                                    attr['device_typ'] == "Gate":
                                covers.append(s_dict['entity_id'])
                            elif attr['device_typ'] == "Switch" or \
                                    attr['device_typ'] == "Pushbutton" or \
                                    attr['device_typ'] == "TimedSwitch":
                                switches.append(s_dict['entity_id'])
                            elif attr['device_typ'] == "LightControllerV2" or \
                                    attr['device_typ'] == "Dimmer":
                                lights.append(s_dict['entity_id'])

                    sensors_analog.sort()
                    sensors_digital.sort()
                    covers.sort()
                    switches.sort()
                    lights.sort()

                    async def create_loxone_group(object_id, name,
                                                  entity_names, visible=True,
                                                  view=False
                                                  ):
                        if visible:
                            visiblity = "true"
                        else:
                            visiblity = "false"
                        if view:
                            view_state = "true"
                        else:
                            view_state = "false"
                        command = {"object_id": object_id,
                                   "entities": entity_names,
                                   "name": name}

                        await hass.services.async_call("group", "set", command)

                    await create_loxone_group("loxone_analog",
                                              "Loxone Analog Sensors",
                                              sensors_analog, True, False)

                    await create_loxone_group("loxone_digital",
                                              "Loxone Digital Sensors",
                                              sensors_digital, True, False)

                    await create_loxone_group("loxone_switches",
                                              "Loxone Switches", switches,
                                              True, False)

                    await create_loxone_group("loxone_covers", "Loxone Covers",
                                              covers, True, False)

                    await create_loxone_group("loxone_lights", "Loxone Lights",
                                              lights, True, False)

                    await create_loxone_group("loxone_group", "Loxone Group",
                                              ["group.loxone_analog",
                                               "group.loxone_digital",
                                               "group.loxone_switches",
                                               "group.loxone_covers",
                                               "group.loxone_lights",
                                               "group.loxone_dimmers"
                                               ],
                                              True, True)
                except:
                    traceback.print_exc()

    res = False

    try:
        res = await lox.async_init()
    except ConnectionError:
        _LOGGER.error("Connection Error")

    if res is True:
        lox.message_call_back = message_callback
        hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_loxone)
        hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_loxone)
        hass.bus.async_listen_once(EVENT_COMPONENT_LOADED, loxone_discovered)

        async def listen_loxone_send(event):
            """Listen for change Events from Loxone Components"""
            try:
                if event.event_type == SENDDOMAIN and isinstance(event.data,
                                                                 dict):
                    value = event.data.get(ATTR_VALUE, DEFAULT)
                    device_uuid = event.data.get(ATTR_UUID, DEFAULT)
                    await lox.send_websocket_command(device_uuid, value)

                elif event.event_type == SECUREDSENDDOMAIN and isinstance(event.data,
                                                                          dict):
                    value = event.data.get(ATTR_VALUE, DEFAULT)
                    device_uuid = event.data.get(ATTR_UUID, DEFAULT)
                    code = event.data.get(ATTR_CODE, DEFAULT)
                    await lox.send_secured__websocket_command(device_uuid, value, code)

            except ValueError:
                traceback.print_exc()

        hass.bus.async_listen(SENDDOMAIN, listen_loxone_send)
        hass.bus.async_listen(SECUREDSENDDOMAIN, listen_loxone_send)

        async def handle_websocket_command(call):
            """Handle websocket command services."""
            value = call.data.get(ATTR_VALUE, DEFAULT)
            device_uuid = call.data.get(ATTR_UUID, DEFAULT)
            await lox.send_websocket_command(device_uuid, value)

        hass.services.async_register(DOMAIN, 'event_websocket_command',
                                     handle_websocket_command)

    else:
        res = False
        _LOGGER.info("Error")
    return res


# Loxone Stuff
def gen_init_vec():
    from Crypto.Random import get_random_bytes
    return get_random_bytes(IV_BYTES)


def gen_key():
    from Crypto.Random import get_random_bytes
    return get_random_bytes(AES_KEY_SIZE)


def time_elapsed_in_seconds():
    return int(round(time.time()))


class LxJsonKeySalt:
    def __init__(self):
        self.key = None
        self.salt = None
        self.response = None
        self.time_elapsed_in_seconds = None

    def read_user_salt_responce(self, reponse):
        js = json.loads(reponse, strict=False)
        value = js['LL']['value']
        self.key = value['key']
        self.salt = value['salt']


class LxToken:
    def __init__(self, token="", vaild_until=""):
        self._token = token
        self._vaild_until = vaild_until

    def get_seconds_to_expire(self):
        start_date = int(
            datetime.strptime("1.1.2009", "%d.%m.%Y").strftime('%s'))
        start_date = int(start_date) + self._vaild_until
        return start_date - int(round(time.time()))

    @property
    def token(self):
        return self._token

    @property
    def vaild_until(self):
        return self._vaild_until

    def set_vaild_until(self, value):
        self._vaild_until = value

    def set_token(self, token):
        self._token = token


class LoxoneEntity(Entity):
    """
    @DynamicAttrs
    """
    def __init__(self, **kwargs):
        self._name = ""
        for key in kwargs:
            if not hasattr(self, key):
                setattr(self, key, kwargs[key])
            else:
                try:
                    setattr(self, key, kwargs[key])
                except:
                    traceback.print_exc()
                    import sys
                    sys.exit(-1)

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, n):
        self._name = n

    @staticmethod
    def _clean_unit(lox_format):
        cleaned_fields = []
        fields = lox_format.split(" ")
        for f in fields:
            _ = f.strip()
            if len(_) > 0:
                cleaned_fields.append(_)

        if len(cleaned_fields) > 1:
            unit = cleaned_fields[1]
            if unit == "%%":
                unit = "%"
            return unit
        return None

    @staticmethod
    def _get_format(lox_format):
        cleaned_fields = []
        fields = lox_format.split(" ")
        for f in fields:
            _ = f.strip()
            if len(_) > 0:
                cleaned_fields.append(_)

        if len(cleaned_fields) > 1:
            return cleaned_fields[0]
        return None

    @property
    def unique_id(self) -> str:
        """Return a unique ID."""
        return self.uuidAction


class LoxWs:
    def __init__(self, user=None,
                 password=None,
                 host="http://192.168.1.225 ",
                 port="8080", token_persist_filename=None,
                 loxconfig=None):
        self._username = user
        self._pasword = password
        self._host = host
        self._port = port
        self._token_refresh_count = TOKEN_REFRESH_RETRY_COUNT
        self._token_persist_filename = token_persist_filename
        self._loxconfig = loxconfig
        self._version = 0
        if self._loxconfig is not None:
            if 'softwareVersion' in self._loxconfig:
                vers = self._loxconfig['softwareVersion']
                if isinstance(vers, list) and len(vers) >= 2:
                    try:
                        self._version = float("{}.{}".format(vers[0], vers[1]))
                    except ValueError:
                        self._version = 0

        if self._token_persist_filename is None:
            self._token_persist_filename = DEFAULT_TOKEN_PERSIST_NAME

        self._iv = gen_init_vec()
        self._key = gen_key()
        self._token = LxToken()
        self._token_valid_until = 0
        self._salt = ""
        self._salt_uesed_count = 0
        self._salt_time_stamp = 0
        self._public_key = None
        self._rsa_cipher = None
        self._session_key = None
        self._ws = None
        self._current_message_typ = None
        self._encryption_ready = False
        self._visual_hash = None
        self._keep_alive_task = None

        self.message_call_back = None
        self._pending = []

        self.connect_retries = 10
        self.connect_delay = 30
        self.state = "CLOSED"
        self._secured_queue = queue.Queue(maxsize=1)

    @property
    def key(self):
        return self._key

    @property
    def iv(self):
        return self._iv

    async def refresh_token(self):
        while True:
            seconds_to_refresh = self._token.get_seconds_to_expire()
            await asyncio.sleep(seconds_to_refresh)
            await self._refresh_token()

    async def decrypt(self, message):
        pass

    async def _refresh_token(self):
        from Crypto.Hash import SHA1, HMAC
        command = "{}".format(CMD_GET_KEY)
        enc_command = await self.encrypt(command)
        await self._ws.send(enc_command)
        message = await self._ws.recv()
        resp_json = json.loads(message)
        token_hash = None
        if 'LL' in resp_json:
            if "value" in resp_json['LL']:
                key = resp_json['LL']['value']
                if key == "":
                    digester = HMAC.new(binascii.unhexlify(key),
                                        self._token.token.encode("utf-8"), SHA1)
                    token_hash = digester.hexdigest()

        if token_hash is not None:
            if self._version < 10.2:
                command = "{}{}/{}".format(CMD_REFRESH_TOKEN, token_hash,
                                           self._username)
            else:
                command = "{}{}/{}".format(CMD_REFRESH_TOKEN_JSON_WEB, token_hash,
                                           self._username)

            enc_command = await self.encrypt(command)
            await self._ws.send(enc_command)
            message = await self._ws.recv()
            resp_json = json.loads(message)

            _LOGGER.debug("Seconds before refresh: {}".format(
                self._token.get_seconds_to_expire()))

            if 'LL' in resp_json:
                if "value" in resp_json['LL']:
                    if "validUntil" in resp_json['LL']['value']:
                        self._token.set_vaild_until(
                            resp_json['LL']['value']['validUntil'])
            self.save_token()

    async def start(self):
        consumer_task = asyncio.ensure_future(self.ws_listen())
        keep_alive_task = asyncio.ensure_future(
            self.keep_alive(KEEP_ALIVE_PERIOD))
        refresh_token_task = asyncio.ensure_future(self.refresh_token())

        self._pending.append(consumer_task)
        self._pending.append(keep_alive_task)
        self._pending.append(refresh_token_task)

        done, pending = await asyncio.wait(
            [consumer_task, keep_alive_task, refresh_token_task],
            return_when=asyncio.FIRST_COMPLETED,
        )

        for task in pending:
            task.cancel()

        if self.state != "STOPPING":
            self.state == "CONNECTING"
            self._pending = []
            for i in range(self.connect_retries):
                _LOGGER.debug("reconnect: {} from {}".format(i + 1, self.connect_retries))
                await self.stop()
                await asyncio.sleep(self.connect_delay)
                res = await self.reconnect()
                if res is True:
                    await self.start()
                    break

    async def reconnect(self):
        return await self.async_init()

    async def stop(self):
        try:
            self.state = "STOPPING"
            if not self._ws.closed:
                await self._ws.close()
            return 1
        except:
            return -1

    async def keep_alive(self, second):
        while True:
            await asyncio.sleep(second)
            if self._encryption_ready:
                await self._ws.send("keepalive")

    async def send_secured(self, device_uuid, value, code):
        from Crypto.Hash import SHA1, HMAC
        pwd_hash_str = code + ":" + self._visual_hash.salt
        m = hashlib.sha1()
        m.update(pwd_hash_str.encode('utf-8'))
        pwd_hash = m.hexdigest().upper()
        digester = HMAC.new(binascii.unhexlify(self._visual_hash.key),
                            pwd_hash.encode("utf-8"), SHA1)

        command = "jdev/sps/ios/{}/{}/{}".format(digester.hexdigest(), device_uuid, value)
        await self._ws.send(command)

    async def send_secured__websocket_command(self, device_uuid, value, code):
        self._secured_queue.put((device_uuid, value, code))
        await self.get_visual_hash()

    async def send_websocket_command(self, device_uuid, value):
        """Send a websocket command to the Miniserver."""
        command = "jdev/sps/io/{}/{}".format(device_uuid, value)
        _LOGGER.debug("send command: {}".format(command))
        await self._ws.send(command)

    async def async_init(self):
        import websockets as wslib
        _LOGGER.debug("try to read token")
        # Read token from file
        try:
            await self.get_token_from_file()
        except IOError:
            _LOGGER.debug("error token read")

        # Get public key from Loxone
        resp = await self.get_public_key()

        if not resp:
            return ERROR_VALUE

        # Init resa cipher
        rsa_gen = self.init_rsa_cipher()
        if not rsa_gen:
            return ERROR_VALUE

        # Generate session key
        session_gen = self.generate_session_key()
        if not session_gen:
            return ERROR_VALUE

        # Exchange keys
        try:
            self._ws = await wslib.connect(
                "ws://{}:{}/ws/rfc6455".format(self._host, self._port),
                timeout=TIMEOUT)
            await self._ws.send(
                "{}{}".format(CMD_KEY_EXCHANGE, self._session_key))

            message = await self._ws.recv()
            await self.parse_loxone_message(message)
            if self._current_message_typ != 0:
                _LOGGER.debug("error by getting the session key response...")
                return ERROR_VALUE

            message = await self._ws.recv()
            resp_json = json.loads(message)
            if 'LL' in resp_json:
                if "Code" in resp_json['LL']:
                    if resp_json['LL']['Code'] != '200':
                        return ERROR_VALUE
            else:
                return ERROR_VALUE

        except ConnectionError:
            _LOGGER.debug("connection error...")
            return ERROR_VALUE

        self._encryption_ready = True

        if self._token is None or self._token.token == "" or \
                self._token.get_seconds_to_expire() < 300:
            res = await self.acquire_token()
        else:
            res = await self.use_token()

        if res is ERROR_VALUE:
            return ERROR_VALUE

        command = "{}".format(CMD_ENABLE_UPDATES)
        enc_command = await self.encrypt(command)
        await self._ws.send(enc_command)
        _ = await self._ws.recv()
        _ = await self._ws.recv()

        self.state = "CONNECTED"
        return True

    async def get_visual_hash(self):
        command = "{}{}".format(CMD_GET_VISUAL_PASSWD, self._username)
        enc_command = await self.encrypt(command)
        await self._ws.send(enc_command)

    async def ws_listen(self):
        """Listen to all commands from the Miniserver."""
        try:
            while True:
                message = await self._ws.recv()
                await self._async_process_message(message)
                await asyncio.sleep(0)
        except:
            pass

    async def _async_process_message(self, message):
        """Process the messages."""
        if len(message) == 8:
            unpacked_data = unpack('ccccI', message)
            self._current_message_typ = int.from_bytes(unpacked_data[1],
                                                       byteorder='big')
            if self._current_message_typ == 6:
                _LOGGER.debug("Keep alive response received...")
        else:
            parsed_data = await self._parse_loxone_message(message)
            _LOGGER.debug("message [type:{}]):{}".format(self._current_message_typ, parsed_data))

            try:
                resp_json = json.loads(parsed_data)
            except TypeError:
                resp_json = None

            # Visual hash and key response
            if resp_json is not None and 'LL' in resp_json:
                if "control" in resp_json['LL'] and "code" in resp_json['LL'] and resp_json['LL']['code'] in [200,
                                                                                                              '200']:
                    if 'value' in resp_json['LL']:
                        if 'key' in resp_json['LL']['value'] and 'salt' in resp_json['LL']['value']:
                            key_and_salt = LxJsonKeySalt()
                            key_and_salt.key = resp_json['LL']['value']['key']
                            key_and_salt.salt = resp_json['LL']['value']['salt']
                            key_and_salt.time_elapsed_in_seconds = time_elapsed_in_seconds()
                            self._visual_hash = key_and_salt

                            while not self._secured_queue.empty():
                                secured_message = self._secured_queue.get()
                                await self.send_secured(secured_message[0], secured_message[1], secured_message[2])

            if self.message_call_back is not None:
                if "LL" not in parsed_data and parsed_data != {}:
                    await self.message_call_back(parsed_data)
            self._current_message_typ = None
            await asyncio.sleep(0)

    async def _parse_loxone_message(self, message):
        """Parser of the Loxone message."""
        event_dict = {}
        if self._current_message_typ == 0:
            event_dict = message
        elif self._current_message_typ == 1:
            pass
        elif self._current_message_typ == 2:
            length = len(message)
            num = length / 24
            start = 0
            end = 24
            for i in range(int(num)):
                packet = message[start:end]
                event_uuid = uuid.UUID(bytes_le=packet[0:16])
                fields = event_uuid.urn.replace("urn:uuid:", "").split("-")
                uuidstr = "{}-{}-{}-{}{}".format(
                    fields[0], fields[1], fields[2], fields[3], fields[4])
                value = unpack('d', packet[16:24])[0]
                event_dict[uuidstr] = value
                start += 24
                end += 24
        elif self._current_message_typ == 3:
            from math import floor
            start = 0

            def get_text(message, start, offset):
                first = start
                second = start + offset
                event_uuid = uuid.UUID(bytes_le=message[first:second])
                first += offset
                second += offset

                icon_uuid_fields = event_uuid.urn.replace("urn:uuid:", "").split("-")
                uuidstr = "{}-{}-{}-{}{}".format(
                    icon_uuid_fields[0], icon_uuid_fields[1], icon_uuid_fields[2], icon_uuid_fields[3],
                    icon_uuid_fields[4])

                icon_uuid = uuid.UUID(bytes_le=message[first:second])
                icon_uuid_fields = icon_uuid.urn.replace("urn:uuid:", "").split("-")
                uuidiconstr = "{}-{}-{}-{}{}".format(icon_uuid_fields[0], icon_uuid_fields[1], icon_uuid_fields[2],
                                                     icon_uuid_fields[3], icon_uuid_fields[4])

                first = second
                second += 4

                text_length = unpack('<I', message[first:second])[0]

                first = second
                second = first + text_length
                message_str = unpack('{}s'.format(text_length), message[first:second])[0]
                start += (floor((4 + text_length + 16 + 16 - 1) / 4) + 1) * 4
                event_dict[uuidstr] = message_str.decode("utf-8")
                return start

            while start < len(message):
                start = get_text(message, start, 16)

        elif self._current_message_typ == 6:
            event_dict["keep_alive"] = "received"
        else:
            self._current_message_typ = 7
        return event_dict

    async def use_token(self):
        token_hash = await self.hash_token()
        if token_hash is ERROR_VALUE:
            return ERROR_VALUE
        command = "{}{}/{}".format(CMD_AUTH_WITH_TOKEN, token_hash,
                                   self._username)
        enc_command = await self.encrypt(command)
        await self._ws.send(enc_command)
        message = await self._ws.recv()
        await self.parse_loxone_message(message)
        message = await self._ws.recv()
        resp_json = json.loads(message)
        if 'LL' in resp_json:
            if "code" in resp_json['LL']:
                if resp_json['LL']['code'] == "200":
                    if "value" in resp_json['LL']:
                        self._token.set_vaild_until(
                            resp_json['LL']['value']['validUntil'])
                    return True
        return ERROR_VALUE

    async def hash_token(self):
        try:
            from Crypto.Hash import SHA1, HMAC
            command = "{}".format(CMD_GET_KEY)
            enc_command = await self.encrypt(command)
            await self._ws.send(enc_command)
            message = await self._ws.recv()
            await self.parse_loxone_message(message)
            message = await self._ws.recv()
            resp_json = json.loads(message)
            if 'LL' in resp_json:
                if "value" in resp_json['LL']:
                    key = resp_json['LL']['value']
                    if key != "":
                        digester = HMAC.new(binascii.unhexlify(key),
                                            self._token.token.encode("utf-8"), SHA1)
                        return digester.hexdigest()
            return ERROR_VALUE
        except:
            return ERROR_VALUE

    async def acquire_token(self):
        _LOGGER.debug("acquire_tokend")
        command = "{}{}".format(CMD_GET_KEY_AND_SALT, self._username)
        enc_command = await self.encrypt(command)

        if not self._encryption_ready or self._ws is None:
            return ERROR_VALUE

        await self._ws.send(enc_command)
        message = await self._ws.recv()
        await self.parse_loxone_message(message)

        message = await self._ws.recv()

        key_and_salt = LxJsonKeySalt()
        key_and_salt.read_user_salt_responce(message)

        new_hash = self.hash_credentials(key_and_salt)

        if self._version < 10.2:
            command = "{}{}/{}/{}/edfc5f9a-df3f-4cad-9dddcdc42c732be2" \
                      "/homeassistant".format(CMD_REQUEST_TOKEN, new_hash,
                                              self._username, TOKEN_PERMISSION)
        else:
            command = "{}{}/{}/{}/edfc5f9a-df3f-4cad-9dddcdc42c732be2" \
                      "/homeassistant".format(CMD_REQUEST_TOKEN_JSON_WEB,
                                              new_hash, self._username,
                                              TOKEN_PERMISSION)

        enc_command = await self.encrypt(command)
        await self._ws.send(enc_command)
        message = await self._ws.recv()
        await self.parse_loxone_message(message)
        message = await self._ws.recv()

        resp_json = json.loads(message)
        if 'LL' in resp_json:
            if "value" in resp_json['LL']:
                if "token" in resp_json['LL']['value'] and "validUntil" in \
                        resp_json['LL']['value']:
                    self._token = LxToken(resp_json['LL']['value']['token'],
                                          resp_json['LL']['value'][
                                              'validUntil'])

        if self.save_token() == ERROR_VALUE:
            return ERROR_VALUE
        return True

    def load_token(self):
        try:
            persist_token = os.path.join(get_default_config_dir(),
                                         self._token_persist_filename)
            try:
                with open(persist_token) as f:
                    try:
                        dict_token = json.load(f)
                    except ValueError:
                        return ERROR_VALUE
            except FileNotFoundError:
                with open(self._token_persist_filename) as f:
                    try:
                        dict_token = json.load(f)
                    except ValueError:
                        return ERROR_VALUE
            self._token.set_token(dict_token['_token'])
            self._token.set_vaild_until(dict_token['_valid_until'])
            _LOGGER.debug("load_token successfully...")
            return True
        except IOError:
            _LOGGER.debug("error load_token...")
            return ERROR_VALUE

    def save_token(self):
        try:
            persist_token = os.path.join(get_default_config_dir(),
                                         self._token_persist_filename)

            dict_token = {"_token": self._token.token,
                          "_valid_until": self._token.vaild_until}
            try:
                with open(persist_token, "w") as write_file:
                    json.dump(dict_token, write_file)
            except FileNotFoundError:
                with open(self._token_persist_filename, "w") as write_file:
                    json.dump(dict_token, write_file)

            _LOGGER.debug("save_token successfully...")
            return True
        except IOError:
            _LOGGER.debug("error save_token...")
            _LOGGER.debug("tokenpath: {}".format(persist_token))
            return ERROR_VALUE

    async def encrypt(self, command):
        from Crypto.Util import Padding
        if not self._encryption_ready:
            return command
        if self._salt != "" and self.new_salt_needed():
            prev_salt = self._salt
            self._salt = self.genarate_salt()
            s = "nextSalt/{}/{}/{}\0".format(prev_salt, self._salt, command)
        else:
            if self._salt == "":
                self._salt = self.genarate_salt()
            s = "salt/{}/{}\0".format(self._salt, command)
        s = Padding.pad(bytes(s, "utf-8"), 16)
        aes_cipher = self.get_new_aes_chiper()
        encrypted = aes_cipher.encrypt(s)
        encoded = b64encode(encrypted)
        encoded_url = req.pathname2url(encoded.decode("utf-8"))
        return CMD_ENCRYPT_CMD + encoded_url

    def hash_credentials(self, key_salt):
        try:
            from Crypto.Hash import SHA1, HMAC
            pwd_hash_str = self._pasword + ":" + key_salt.salt
            m = hashlib.sha1()
            m.update(pwd_hash_str.encode('utf-8'))
            pwd_hash = m.hexdigest().upper()
            pwd_hash = self._username + ":" + pwd_hash
            digester = HMAC.new(binascii.unhexlify(key_salt.key),
                                pwd_hash.encode("utf-8"), SHA1)
            _LOGGER.debug("hash_credentials successfully...")
            return digester.hexdigest()
        except ValueError:
            _LOGGER.debug("error hash_credentials...")
            return None

    def genarate_salt(self):
        from Crypto.Random import get_random_bytes
        salt = get_random_bytes(SALT_BYTES)
        salt = binascii.hexlify(salt).decode("utf-8")
        salt = req.pathname2url(salt)
        self._salt_time_stamp = time_elapsed_in_seconds()
        self._salt_uesed_count = 0
        return salt

    def new_salt_needed(self):
        self._salt_uesed_count += 1
        if self._salt_uesed_count > SALT_MAX_USE_COUNT or time_elapsed_in_seconds() - self._salt_time_stamp > SALT_MAX_AGE_SECONDS:
            return True
        return False

    async def parse_loxone_message(self, message):
        if len(message) == 8:
            try:
                unpacked_data = unpack('ccccI', message)
                self._current_message_typ = int.from_bytes(unpacked_data[1],
                                                           byteorder='big')
                _LOGGER.debug("parse_loxone_message successfully...")
            except ValueError:
                _LOGGER.debug("error parse_loxone_message...")

    def generate_session_key(self):
        try:
            aes_key = binascii.hexlify(self._key).decode("utf-8")
            iv = binascii.hexlify(self._iv).decode("utf-8")
            sess = aes_key + ":" + iv
            sess = self._rsa_cipher.encrypt(bytes(sess, "utf-8"))
            self._session_key = b64encode(sess).decode("utf-8")
            _LOGGER.debug("generate_session_key successfully...")
            return True
        except KeyError:
            _LOGGER.debug("error generate_session_key...")
            return False

    def get_new_aes_chiper(self):
        try:
            from Crypto.Cipher import AES
            _new_aes = AES.new(self._key, AES.MODE_CBC, self._iv)
            _LOGGER.debug("get_new_aes_chiper successfully...")
            return _new_aes
        except ValueError:
            _LOGGER.debug("error get_new_aes_chiper...")
            return None

    def init_rsa_cipher(self):
        try:
            from Crypto.Cipher import PKCS1_v1_5
            from Crypto.PublicKey import RSA
            self._public_key = self._public_key.replace(
                "-----BEGIN CERTIFICATE-----",
                "-----BEGIN PUBLIC KEY-----\n")
            public_key = self._public_key.replace(
                "-----END CERTIFICATE-----",
                "\n-----END PUBLIC KEY-----\n")
            self._rsa_cipher = PKCS1_v1_5.new(RSA.importKey(public_key))
            _LOGGER.debug("init_rsa_cipher successfully...")
            return True
        except KeyError:
            _LOGGER.debug("init_rsa_cipher error...")
            _LOGGER.debug("{}".format(traceback.print_exc()))
            return False

    async def get_public_key(self):
        command = "http://{}:{}/{}".format(self._host, self._port,
                                           CMD_GET_PUBLIC_KEY)
        _LOGGER.debug("try to get public key: {}".format(command))

        try:
            response = await requests.get(command, auth=(self._username, self._pasword), timeout=TIMEOUT)
        except:
            return False

        if response.status_code != 200:
            _LOGGER.debug(
                "error get_public_key: {}".format(response.status_code))
            return False
        try:
            resp_json = json.loads(response.text)
            if 'LL' in resp_json and 'value' in resp_json['LL']:
                self._public_key = resp_json['LL']['value']
                _LOGGER.debug("get_public_key successfully...")
            else:
                _LOGGER.debug("public key load error")
                return False
        except ValueError:
            _LOGGER.debug("public key load error")
            return False
        return True

    async def get_token_from_file(self):
        try:
            persist_token = os.path.join(get_default_config_dir(),
                                         self._token_persist_filename)
            if os.path.exists(persist_token):
                if self.load_token():
                    _LOGGER.debug(
                        "token successfully loaded from file: {}".format(
                            persist_token))
        except FileExistsError:
            _LOGGER.debug("error loading token {}".format(persist_token))
            _LOGGER.debug("{}".format(traceback.print_exc()))