text: #     Copyright 2021. FastyBird s.r.o.
  #
  #     Licensed 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.
  
  """
  Shelly connector module
  """
  
  # Python base dependencies
  import logging
  import re
  import uuid
  from datetime import datetime
  from threading import Thread
  from typing import Optional, Set, Tuple, Union
  
  # App dependencies
  from devices_module.items import (
      ChannelItem,
      ChannelPropertyItem,
      DeviceItem,
      DevicePropertyItem,
      ShellyConnectorItem,
  )
  from devices_module.repositories import (
      ChannelsPropertiesRepository,
      ChannelsRepository,
      ConnectorsRepository,
      DevicesPropertiesRepository,
      DevicesRepository,
  )
  from kink import inject
  from modules_metadata.devices_module import ConnectorType, DeviceConnectionState
  from modules_metadata.types import ButtonPayload, DataType, SwitchPayload
  
  # App libs
  from miniserver_gateway.connectors.iconnector import IConnector
  from miniserver_gateway.connectors.queue_items import (
      IQueueItem,
      PropagateChannelPropertyExpectedValueQueueItem,
      PropagatePropertyExpectedValueQueueItem,
  )
  from miniserver_gateway.connectors.shelly.clients.client import Client, ClientFactory
  from miniserver_gateway.connectors.shelly.consumers.consumer import Consumer
  from miniserver_gateway.connectors.shelly.events.listeners import EventsListener
  from miniserver_gateway.connectors.shelly.logger import Logger
  from miniserver_gateway.connectors.shelly.receivers.receiver import Receiver
  from miniserver_gateway.connectors.shelly.registry.model import (
      AttributesRegistry,
      BlocksRegistry,
      DevicesRegistry,
      SensorsRegistry,
  )
  from miniserver_gateway.connectors.shelly.types import (
      ClientType,
      DeviceAttribute,
      DeviceDescriptionSource,
      SensorType,
      SensorUnit,
  )
  from miniserver_gateway.storages.devices.repository import (
      DevicesRepository as PropertiesStatesRepository,
  )
  
  
  class ShellyConnector:  # pylint: disable=too-many-instance-attributes,too-many-public-methods
      """
      Shelly connector service
  
      @package        FastyBird:ShellyConnectorPlugin!
      @module         connector
  
      @author         Adam Kadlec <adam.kadlec@fastybird.com>
      """
  
      __stopped: bool = False
  
      __receiver: Receiver
      __consumer: Consumer
  
      __devices_registry: DevicesRegistry
      __attributes_registry: AttributesRegistry
      __blocks_registry: BlocksRegistry
      __sensors_registry: SensorsRegistry
  
      __client: Client
      __client_factory: ClientFactory
  
      __events_listener: EventsListener
  
      __logger: Logger
  
      # -----------------------------------------------------------------------------
  
      def __init__(  # pylint: disable=too-many-arguments
          self,
          receiver: Receiver,
          consumer: Consumer,
          devices_registry: DevicesRegistry,
          attributes_registry: AttributesRegistry,
          blocks_registry: BlocksRegistry,
          sensors_registry: SensorsRegistry,
          client: Client,
          client_factory: ClientFactory,
          events_listener: EventsListener,
          logger: Logger,
      ) -> None:
          self.__receiver = receiver
          self.__consumer = consumer
  
          self.__devices_registry = devices_registry
          self.__attributes_registry = attributes_registry
          self.__blocks_registry = blocks_registry
          self.__sensors_registry = sensors_registry
  
          self.__client = client
          self.__client_factory = client_factory
  
          self.__events_listener = events_listener
  
          self.__logger = logger
  
      # -----------------------------------------------------------------------------
  
      def configure_client(  # pylint: disable=too-many-arguments
          self,
          client_id: uuid.UUID,
      ) -> None:
          """Configure Shelly clients & append them to client proxy"""
          self.__client_factory.create(
              client_id=client_id,
              client_type=ClientType.COAP,
          )
  
          self.__client_factory.create(
              client_id=client_id,
              client_type=ClientType.MDNS,
          )
  
          self.__client_factory.create(
              client_id=client_id,
              client_type=ClientType.HTTP,
          )
  
      # -----------------------------------------------------------------------------
  
      def enable_client(self, client_id: uuid.UUID) -> bool:
          """Enable client"""
          return self.__client.enable_client(client_id=client_id)
  
      # -----------------------------------------------------------------------------
  
      def disable_client(self, client_id: uuid.UUID) -> bool:
          """Disable client connector"""
          return self.__client.disable_client(client_id=client_id)
  
      # -----------------------------------------------------------------------------
  
      def remove_client(self, client_id: uuid.UUID) -> bool:
          """Remove client from connector"""
          return self.__client.remove_client(client_id=client_id)
  
      # -----------------------------------------------------------------------------
  
      def reset_clients(self) -> None:
          """Reset all connector clients"""
          self.__client.reset_clients()
  
      # -----------------------------------------------------------------------------
  
      def initialize_device(  # pylint: disable=too-many-arguments,too-many-locals
          self,
          client_id: uuid.UUID,
          device_id: uuid.UUID,
          device_identifier: str,
          device_type: Optional[str] = None,
          device_enabled: bool = False,
      ) -> None:
          """Configure device record in connector registry"""
          self.__devices_registry.append(
              description_source=DeviceDescriptionSource.MANUAL,
              client_id=client_id,
              device_id=device_id,
              device_identifier=device_identifier,
              device_type=device_type,
              device_enabled=device_enabled,
          )
  
      # -----------------------------------------------------------------------------
  
      def remove_device(self, device_id: uuid.UUID) -> None:
          """Remove device from connector registry"""
          self.__devices_registry.remove(device_id=device_id)
  
      # -----------------------------------------------------------------------------
  
      def reset_devices(self, client_id: Optional[uuid.UUID] = None) -> None:
          """Reset devices registry to initial state"""
          self.__devices_registry.reset(client_id=client_id)
  
      # -----------------------------------------------------------------------------
  
      def initialize_device_attribute(  # pylint: disable=too-many-arguments
          self,
          device_id: uuid.UUID,
          attribute_id: uuid.UUID,
          attribute_type: str,
          attribute_value: Union[str, bool, None],
      ) -> None:
          """Configure device attribute record in connector registry"""
          if not DeviceAttribute.has_value(attribute_type):
              return
  
          attribute_record = self.__attributes_registry.append(
              device_id=device_id,
              attribute_id=attribute_id,
              attribute_type=DeviceAttribute(attribute_type),
              attribute_value=attribute_value,
          )
  
          if attribute_type == DeviceAttribute.STATE.value:
              self.__attributes_registry.set_value(attribute=attribute_record, value=DeviceConnectionState.UNKNOWN.value)
  
      # -----------------------------------------------------------------------------
  
      def reset_device_attributes(self, device_id: Optional[uuid.UUID] = None) -> None:
          """Reset devices attributes registry to initial state"""
          self.__attributes_registry.reset(device_id=device_id)
  
      # -----------------------------------------------------------------------------
  
      def remove_attribute(self, attribute_id: uuid.UUID) -> None:
          """Remove device attribute from connector registry"""
          self.__attributes_registry.remove(attribute_id=attribute_id)
  
      # -----------------------------------------------------------------------------
  
      def initialize_device_block(  # pylint: disable=too-many-arguments
          self,
          device_id: uuid.UUID,
          block_id: uuid.UUID,
          block_identifier: int,
          block_description: str,
      ) -> None:
          """Configure device block record in connector registry"""
          self.__blocks_registry.append(
              device_id=device_id,
              block_id=block_id,
              block_identifier=block_identifier,
              block_description=block_description,
          )
  
      # -----------------------------------------------------------------------------
  
      def reset_device_blocks(self, device_id: Optional[uuid.UUID] = None) -> None:
          """Reset devices blocks registry to initial state"""
          self.__blocks_registry.reset(device_id=device_id)
  
      # -----------------------------------------------------------------------------
  
      def remove_block(self, block_id: uuid.UUID) -> None:
          """Remove device block from connector registry"""
          self.__blocks_registry.remove(block_id=block_id)
  
      # -----------------------------------------------------------------------------
  
      def initialize_block_sensor(  # pylint: disable=too-many-arguments
          self,
          block_id: uuid.UUID,
          sensor_id: uuid.UUID,
          sensor_identifier: int,
          sensor_type: SensorType,
          sensor_description: str,
          sensor_unit: Optional[SensorUnit] = None,
          sensor_data_type: Optional[DataType] = None,
          sensor_value_format: Union[
              Set[str], Tuple[Optional[int], Optional[int]], Tuple[Optional[float], Optional[float]], None
          ] = None,
          sensor_value_invalid: Union[str, int, float, bool, None] = None,
          sensor_queryable: bool = False,
          sensor_settable: bool = False,
      ) -> None:
          """Configure block sensor record in connector registry"""
          self.__sensors_registry.append(
              block_id=block_id,
              sensor_id=sensor_id,
              sensor_identifier=sensor_identifier,
              sensor_type=sensor_type,
              sensor_description=sensor_description,
              sensor_unit=sensor_unit,
              sensor_data_type=sensor_data_type,
              sensor_value_format=sensor_value_format,
              sensor_value_invalid=sensor_value_invalid,
              sensor_queryable=sensor_queryable,
              sensor_settable=sensor_settable,
          )
  
      # -----------------------------------------------------------------------------
  
      def reset_block_sensors(self, block_id: Optional[uuid.UUID] = None) -> None:
          """Reset block sensors registry to initial state"""
          self.__sensors_registry.reset(block_id=block_id)
  
      # -----------------------------------------------------------------------------
  
      def remove_sensor(self, sensor_id: uuid.UUID) -> None:
          """Remove block sensor from connector registry"""
          self.__sensors_registry.remove(sensor_id=sensor_id)
  
      # -----------------------------------------------------------------------------
  
      def start(self) -> None:
          """Start connector services"""
          self.__stopped = False
  
          self.__client.start()
  
          self.__events_listener.open()
  
          self.__logger.info(
              "Connector has been started",
              extra={
                  "connector": ConnectorType.SHELLY.value,
              },
          )
  
      # -----------------------------------------------------------------------------
  
      def stop(self) -> None:
          """Close all opened connections & stop connector thread"""
          self.__client.stop()
  
          for state_attribute_record in self.__attributes_registry.get_all_by_type(attribute_type=DeviceAttribute.STATE):
              self.__attributes_registry.set_value(
                  attribute=state_attribute_record,
                  value=DeviceConnectionState.DISCONNECTED.value,
              )
  
          self.__events_listener.close()
  
          self.__logger.info(
              "Connector has been stopped",
              extra={
                  "connector": ConnectorType.SHELLY.value,
              },
          )
  
          self.__stopped = True
  
      # -----------------------------------------------------------------------------
  
      def write_sensor_value(
          self,
          sensor_id: uuid.UUID,
          write_value: Union[str, int, float, bool, datetime, ButtonPayload, SwitchPayload, None],
      ) -> bool:
          """Write expected value to sensor"""
          if self.__stopped:
              self.__logger.warning(
                  "Connector is stopped, value can't be written",
                  extra={
                      "connector": ConnectorType.SHELLY.value,
                      "sensor": sensor_id,
                  },
              )
  
              return False
  
          sensor_record = self.__sensors_registry.get_by_id(sensor_id=sensor_id)
  
          if sensor_record is not None and (
              isinstance(write_value, (str, int, float, bool, ButtonPayload, SwitchPayload)) or write_value is None
          ):
              self.__sensors_registry.set_expected_value(sensor=sensor_record, value=write_value)
  
              return True
  
          return False
  
      # -----------------------------------------------------------------------------
  
      def has_unfinished_tasks(self) -> bool:
          """Check if connector has some unfinished task"""
          return not self.__consumer.is_empty() or not self.__receiver.is_empty()
  
      # -----------------------------------------------------------------------------
  
      def loop(self) -> None:
          """Run connector service"""
          if self.__stopped and not self.has_unfinished_tasks():
              self.__logger.warning(
                  "Connector is stopped and can't process another requests",
                  extra={
                      "connector": ConnectorType.SHELLY.value,
                  },
              )
  
              return
  
          self.__receiver.loop()
          self.__consumer.loop()
  
          if self.__stopped:
              return
  
          self.__client.loop()
          self.__devices_registry.check_timeout()
          self.__sensors_registry.check_write()
  
  
  @inject
  class ShellyConnectorWrapper(IConnector, Thread):  # pylint: disable=too-many-instance-attributes
      """
      Shelly connector service
  
      @package        FastyBird:MiniServer!
      @module         connector
  
      @author         Adam Kadlec <adam.kadlec@fastybird.com>
      """
  
      __stopped: bool = True
  
      __connector: ShellyConnector
  
      __connectors_repository: ConnectorsRepository
      __devices_repository: DevicesRepository
      __devices_properties_repository: DevicesPropertiesRepository
      __channels_repository: ChannelsRepository
      __channels_properties_repository: ChannelsPropertiesRepository
      __properties_states_repository: PropertiesStatesRepository
  
      __logger: logging.Logger
  
      # -----------------------------------------------------------------------------
  
      def __init__(  # pylint: disable=too-many-arguments
          self,
          connector: ShellyConnector,
          connectors_repository: ConnectorsRepository,
          devices_repository: DevicesRepository,
          devices_properties_repository: DevicesPropertiesRepository,
          channels_repository: ChannelsRepository,
          channels_properties_repository: ChannelsPropertiesRepository,
          properties_states_repository: PropertiesStatesRepository,
          logger: logging.Logger,
      ) -> None:
          Thread.__init__(self, name="Shelly connector thread", daemon=True)
  
          self.__connector = connector
  
          self.__connectors_repository = connectors_repository
          self.__devices_repository = devices_repository
          self.__devices_properties_repository = devices_properties_repository
          self.__channels_repository = channels_repository
          self.__channels_properties_repository = channels_properties_repository
          self.__properties_states_repository = properties_states_repository
  
          self.__logger = logger
  
      # -----------------------------------------------------------------------------
  
      def start(self) -> None:
          """Start connector service"""
          self.__initialize()
  
          self.__connector.start()
  
          self.__stopped = False
  
          Thread.start(self)
  
      # -----------------------------------------------------------------------------
  
      def stop(self) -> None:
          """Stop connector service"""
          self.__connector.stop()
  
          self.__stopped = True
  
      # -----------------------------------------------------------------------------
  
      def is_alive(self) -> bool:
          """Check if connector has some task to process"""
          return self.__connector.has_unfinished_tasks() or Thread.is_alive(self)
  
      # -----------------------------------------------------------------------------
  
      def process_queue_item(self, item: IQueueItem) -> None:
          """Process item popped from queue"""
          if isinstance(item, PropagatePropertyExpectedValueQueueItem):
              if isinstance(item, PropagateChannelPropertyExpectedValueQueueItem):
                  if not self.__connector.write_sensor_value(sensor_id=item.property_id, write_value=item.expected_value):
                      self.__logger.warning(
                          "Expected value can't be written into sensor",
                          extra={
                              "connector": ConnectorType.SHELLY.value,
                          },
                      )
  
      # -----------------------------------------------------------------------------
  
      def run(self) -> None:
          """Process connector loop"""
          while True:
              # All tasks have to be processed before thread is closed
              if self.__stopped and not self.__connector.has_unfinished_tasks():
                  break
  
              self.__connector.loop()
  
      # -----------------------------------------------------------------------------
  
      def clone(self) -> "ShellyConnectorWrapper":
          """Restart connector thread"""
          self.__logger.debug(
              "Restarting connector thread",
              extra={
                  "connector": ConnectorType.SHELLY.value,
              },
          )
  
          return ShellyConnectorWrapper(
              connector=self.__connector,
              connectors_repository=self.__connectors_repository,
              devices_repository=self.__devices_repository,
              devices_properties_repository=self.__devices_properties_repository,
              channels_repository=self.__channels_repository,
              channels_properties_repository=self.__channels_properties_repository,
              properties_states_repository=self.__properties_states_repository,
              logger=self.__logger,
          )
  
      # -----------------------------------------------------------------------------
  
      def __initialize(self) -> None:
          """Initialize all configured Shelly connectors"""
          self.__connector.reset_clients()
  
          # Process all configured connectors
          for connector in self.__connectors_repository:
              if isinstance(connector, ShellyConnectorItem) and connector.enabled is True:
                  self.__connector.configure_client(
                      client_id=connector.connector_id,
                  )
  
                  self.__initialize_devices(connector_id=connector.connector_id)
  
      # -----------------------------------------------------------------------------
  
      def __initialize_devices(self, connector_id: uuid.UUID) -> None:
          self.__connector.reset_devices(client_id=connector_id)
  
          # Process all connector devices...
          for device in self.__devices_repository.get_all_by_connector(connector_id=connector_id):
              if self.__initialize_device(connector_id=connector_id, device_item=device):
                  self.__initialize_device_properties(device_id=device.device_id)
                  self.__initialize_channels(device_id=device.device_id)
  
      # -----------------------------------------------------------------------------
  
      def __initialize_device(self, connector_id: uuid.UUID, device_item: DeviceItem) -> bool:
          self.__connector.initialize_device(
              client_id=connector_id,
              device_id=device_item.device_id,
              device_identifier=device_item.identifier,
              device_type=device_item.hardware_model,
              device_enabled=device_item.enabled,
          )
  
          self.__logger.debug(
              "Connector's device was initialized",
              extra={
                  "connector": {
                      "type": ConnectorType.SHELLY.value,
                      "id": connector_id.__str__(),
                  },
                  "device": device_item.device_id.__str__(),
              },
          )
  
          return True
  
      # -----------------------------------------------------------------------------
  
      def __initialize_device_properties(self, device_id: uuid.UUID) -> None:
          self.__connector.reset_device_attributes(device_id=device_id)
  
          # Process all device properties...
          for device_property in self.__devices_properties_repository.get_all_by_device(device_id=device_id):
              self.__initialize_device_property_property(property_item=device_property)
  
      # -----------------------------------------------------------------------------
  
      def __initialize_device_property_property(self, property_item: DevicePropertyItem) -> bool:
          attribute_value = self.__properties_states_repository.get_property_actual_value(property_item=property_item)
  
          self.__connector.initialize_device_attribute(
              device_id=property_item.device_id,
              attribute_id=property_item.property_id,
              attribute_type=property_item.identifier,
              attribute_value=attribute_value if isinstance(attribute_value, (str, bool)) else None,
          )
  
          return True
  
      # -----------------------------------------------------------------------------
  
      def __initialize_channels(self, device_id: uuid.UUID) -> None:
          self.__connector.reset_device_blocks(device_id=device_id)
  
          # Process all device channels...
          for channel in self.__channels_repository.get_all_by_device(device_id=device_id):
              if self.__initialize_channel(channel_item=channel):
                  self.__initialize_channel_properties(channel_id=channel.channel_id)
  
      # -----------------------------------------------------------------------------
  
      def __initialize_channel(self, channel_item: ChannelItem) -> bool:
          match = re.compile("(?P<identifier>[0-9]+)_(?P<description>[a-zA-Z0-9_]+)")
  
          test = match.fullmatch(channel_item.identifier)
  
          if test is None:
              device_item = self.__devices_repository.get_by_id(device_id=channel_item.device_id)
  
              self.__logger.debug(
                  "Device's channel couldn't be initialized",
                  extra={
                      "connector": {
                          "type": ConnectorType.SHELLY.value,
                          "id": device_item.connector_id.__str__()
                          if device_item is not None and device_item.connector_id is not None
                          else None,
                      },
                      "device": device_item.device_id.__str__() if device_item is not None else None,
                      "channel": channel_item.channel_id.__str__(),
                  },
              )
  
              return False
  
          self.__connector.initialize_device_block(
              device_id=channel_item.device_id,
              block_id=channel_item.channel_id,
              block_identifier=int(test.group("identifier")),
              block_description=test.group("description"),
          )
  
          return True
  
      # -----------------------------------------------------------------------------
  
      def __initialize_channel_properties(self, channel_id: uuid.UUID) -> None:
          self.__connector.reset_block_sensors(block_id=channel_id)
  
          # Process all device channels...
          for channel_property in self.__channels_properties_repository.get_all_by_channel(channel_id=channel_id):
              self.__initialize_channel_property(property_item=channel_property)
  
      # -----------------------------------------------------------------------------
  
      def __initialize_channel_property(self, property_item: ChannelPropertyItem) -> bool:
          match = re.compile("(?P<identifier>[0-9]+)_(?P<type>[a-zA-Z]{1,3})_(?P<description>[a-zA-Z0-9]+)")
  
          test = match.fullmatch(property_item.identifier)
  
          if (
              test is None
              or not SensorType.has_value(test.group("type"))
              or (property_item.unit is not None and not SensorUnit.has_value(property_item.unit))
          ):
              channel_item = self.__channels_repository.get_by_id(channel_id=property_item.channel_id)
              device_item = (
                  self.__devices_repository.get_by_id(device_id=channel_item.device_id)
                  if channel_item is not None
                  else None
              )
  
              self.__logger.debug(
                  "Device's channel's property couldn't be initialized",
                  extra={
                      "connector": {
                          "type": ConnectorType.SHELLY.value,
                          "id": device_item.connector_id.__str__()
                          if device_item is not None and device_item.connector_id is not None
                          else None,
                      },
                      "device": device_item.device_id.__str__() if device_item is not None else None,
                      "channel": channel_item.channel_id.__str__() if channel_item is not None else None,
                      "property": property_item.property_id.__str__(),
                  },
              )
  
              return False
  
          self.__connector.initialize_block_sensor(
              block_id=property_item.channel_id,
              sensor_id=property_item.property_id,
              sensor_identifier=int(test.group("identifier")),
              sensor_type=SensorType(test.group("type")),
              sensor_description=test.group("description"),
              sensor_unit=SensorUnit(property_item.unit) if property_item.unit is not None else None,
              sensor_data_type=property_item.data_type,
              sensor_value_format=property_item.format,
              sensor_value_invalid=property_item.invalid,
              sensor_queryable=property_item.queryable,
              sensor_settable=property_item.settable,
          )
  
          return True
  