text: """fand server"""
  
  import abc
  import argparse
  import concurrent.futures
  import configparser
  import datetime
  import enum
  import logging
  import os
  import signal
  import socket
  from typing import (Callable, Dict, Iterable, Iterator, List, NoReturn,
                      Optional)
  
  import psutil
  
  import pySMART as pysmart
  
  import fand.communication as com
  import fand.util as util
  from fand.exceptions import (
      CommunicationError, ListeningError, ServerNoConfigError,
      ShelfNotFoundError, ShelfPwmBadValue, ShelfPwmExpireBadValue,
      ShelfRpmBadValue, ShelfTemperatureBadValue,
  )
  
  
  # Constants
  # Module docstring
  __DOCSTRING__ = __doc__
  # Logger to use
  logger = logging.getLogger(__name__)
  
  # Global variables
  # Dictionnary of shelves, key = shelf ID
  __SHELVES__: Dict[str, 'Shelf'] = {}
  
  
  class Device:
      """Class handling devices to get temperature from
  
      :param serial: Device serial number
      :param position: Device positionning information
      """
      serial: str
      position: str
      __device: 'Device._DeviceWrapper'
  
      def __init__(self, serial: str, position: str) -> None:
          #: Device serial number
          self.serial = serial
          #: Device positionning information
          self.position = position
          self.__device = self.find()
          logger.info("New device %s created", self)
  
      def __str__(self) -> str:
          return f"{str(self.serial)} at {str(self.position)}"
  
      def find(self) -> 'Device._DeviceWrapper':
          """Search device on the system"""
          if self.serial == 'cpu':
              if psutil.sensors_temperatures().get('coretemp') is not None:
                  logger.debug("Identified host CPU")
                  return Device._CpuWrapper()
              logger.debug("Cannot access CPU informations")
              return Device._NoneDevice()
          for device in pysmart.DeviceList().devices:
              if device.serial == self.serial:
                  if not device.is_ssd:
                      logger.debug("Identified HDD %s", self.serial)
                      return Device._HddWrapper(device)
                  logger.debug("Identified SSD %s", self.serial)
                  return Device._SsdWrapper(device)
          logger.error("Device not found: %s", self.serial)
          return Device._NoneDevice()
  
      def update(self) -> None:
          """Update device informations"""
          logger.debug("Updating device %s", self)
          self.__device.update()
          if self.__device.serial != self.serial:
              self.__device = self.find()
  
      @property
      def temperature(self) -> float:
          """Current drive temperature"""
          return self.__device.temperature
  
      @property
      def type(self) -> 'Device.DeviceType':
          """DeviceType"""
          return self.__device.type
  
      class DeviceType(enum.Enum):
          """Enumeration of device types, to identify Device objects"""
          #: Unknown device
          NONE = enum.auto()
          #: HDD
          HDD = enum.auto()
          #: SSD
          SSD = enum.auto()
          #: System CPU
          CPU = enum.auto()
  
      class _DeviceWrapper(abc.ABC):
          """Abstract class for device wrappers"""
          @abc.abstractmethod
          def update(self) -> None:
              """Update the device informations"""
  
          @property
          @abc.abstractmethod
          def temperature(self) -> float:
              """Current device temperature"""
  
          @property
          @abc.abstractmethod
          def serial(self) -> str:
              """Current device serial"""
  
          @property
          @abc.abstractmethod
          def type(self) -> 'Device.DeviceType':
              """Device type"""
  
      class _HddWrapper(_DeviceWrapper):
          """Wrapper class for HDDs"""
          pysmart: pysmart.Device
  
          def __init__(self, device: pysmart.Device) -> None:
              self.pysmart = device
  
          def update(self) -> None:
              self.pysmart.update()
  
          @property
          def temperature(self) -> float:
              return self.pysmart.temperature
  
          @property
          def serial(self) -> str:
              return self.pysmart.serial
  
          @property
          def type(self) -> 'Device.DeviceType':
              return Device.DeviceType.HDD
  
      class _SsdWrapper(_DeviceWrapper):
          """wrapper class for SSDs"""
          pysmart: pysmart.Device
  
          def __init__(self, device: pysmart.Device) -> None:
              self.pysmart = device
  
          def update(self) -> None:
              self.pysmart.update()
  
          @property
          def temperature(self) -> float:
              return self.pysmart.temperature
  
          @property
          def serial(self) -> str:
              return self.pysmart.serial
  
          @property
          def type(self) -> 'Device.DeviceType':
              return Device.DeviceType.SSD
  
      class _CpuWrapper(_DeviceWrapper):
          """Wrapper class for host CPU"""
          def update(self) -> None:
              pass
  
          @property
          def temperature(self) -> float:
              return max(temp.current
                         for temp in psutil.sensors_temperatures()['coretemp'])
  
          @property
          def serial(self) -> str:
              return 'cpu'
  
          @property
          def type(self) -> 'Device.DeviceType':
              return Device.DeviceType.CPU
  
      class _NoneDevice(_DeviceWrapper):
          """Wrapper for missing devices"""
          def update(self) -> None:
              pass
  
          @property
          def temperature(self) -> float:
              return 0
  
          @property
          def serial(self) -> str:
              return ''
  
          @property
          def type(self) -> 'Device.DeviceType':
              return Device.DeviceType.NONE
  
  
  class Shelf:
      """Class handling shelf data
  
      :param identifier: Shelf identifier (name)
      :param devices: Iterable of Device objects
      :param sleep_time: How many seconds to wait between each shelf update
      :param hdd_temps: Dictionnary in the format ``temperature: speed``,
          temperature in Celcius, speed in percent, must have a 0 deg key
      :param ssd_temps: Dictionnary in the format ``temperature: speed``,
          temperature in Celcius, speed in percent, must have a 0 deg key
      :param cpu_temps: Dictionnary in the format ``temperature: speed``,
          temperature in Celcius, speed in percent, must have a 0 deg key
  
      :raises ShelfTemperatureBadValue: One of the temps dictionnary is invalid
      """
      identifier: str
      __devices: Dict[str, Device]
      __temperatures: Dict[Device.DeviceType, Dict[float, float]]
      __pwm: float
      __pwm_override: Optional[float]
      sleep_time: float
  
      def __init__(
              self,
              identifier: str,
              devices: Iterable[Device],
              sleep_time: float = 60,
              hdd_temps: Optional[Dict[float, float]] = None,
              ssd_temps: Optional[Dict[float, float]] = None,
              cpu_temps: Optional[Dict[float, float]] = None,
      ) -> None:
          logger.debug("Creating new shelf %s", identifier)
          self.identifier = identifier
          self.__devices = {device.serial: device for device in devices}
          self.__temperatures = {
              Device.DeviceType.NONE: {0: 0},
              Device.DeviceType.HDD: {0: 0} if hdd_temps is None else hdd_temps,
              Device.DeviceType.SSD: {0: 0} if ssd_temps is None else ssd_temps,
              Device.DeviceType.CPU: {0: 0} if cpu_temps is None else cpu_temps,
          }
          for dev_type, dev_temps in self.__temperatures.items():
              if dev_temps.get(0) is None:
                  logger.critical("%s has no 0 temperature configured: %s",
                                  dev_type, dev_temps)
                  raise ShelfTemperatureBadValue(
                      f"{dev_type} has no 0 temperature key"
                  )
          self.__pwm = 100
          self.rpm = 0
          self.__pwm_override = None
          self.pwm_expire = None
          self.sleep_time = sleep_time
  
      def __str__(self) -> str:
          return self.identifier
  
      def __iter__(self) -> Iterator:
          return iter(self.__devices.values())
  
      @property
      def rpm(self) -> float:
          """Shelf fan speed RPM
  
          :raises ShelfRpmBadValue: Invalid value
          """
          return self.__rpm
  
      @rpm.setter
      def rpm(self, speed: float) -> None:
          if speed < 0:
              raise ShelfRpmBadValue("RPM cannot be below zero")
          self.__rpm = speed
  
      @property
      def pwm(self) -> float:
          """Get shelf PWM value
  
          Reading get the effective PWM value.
          Changing override the PWM value.
  
          :raises ShelfPwmBadValue: Invalid value
          """
          now = datetime.datetime.now(datetime.timezone.utc)
          if self.__pwm_override is not None and \
                  (self.__pwm_expire is None or self.__pwm_expire > now):
              return self.__pwm_override
          return self.__pwm
  
      @pwm.setter
      def pwm(self, speed: Optional[float] = None) -> None:
          if speed is not None and (speed < 0 or speed > 100):
              raise ShelfPwmBadValue("PWM must be between 0 and 100")
          self.__pwm_override = speed
  
      @property
      def pwm_expire(self) -> Optional[datetime.datetime]:
          """Set the PWM override expiration date, defaults to local timezone
  
          :raises ShelfPwmExpireBadValue: Invalid value
          """
          return self.__pwm_expire
  
      @pwm_expire.setter
      def pwm_expire(self, date: Optional[datetime.datetime] = None) -> None:
          if date is None:
              self.__pwm_expire = date
              return
          if date.tzinfo is None:
              localtz = datetime.datetime.now().astimezone().tzinfo
              date = date.replace(tzinfo=localtz)
          now = datetime.datetime.now(date.tzinfo)
          if date < now:
              raise ShelfPwmExpireBadValue(f"{date} is in the past")
          self.__pwm_expire = date
  
      def update(self) -> None:
          """Update shelf data"""
          logger.info("Updating shelf %s", self)
          # Update all drives
          for device in self.__devices.values():
              device.update()
  
          # temp_lists dict: `DeviceType: list of temperatures`
          temp_lists: Dict[Device.DeviceType, List[float]] = \
              {dev_type: [] for dev_type in Device.DeviceType}
          for device in self:
              if device.type != Device.DeviceType.NONE:
                  temp_lists[device.type].append(device.temperature)
          logger.debug("Temperatures are: %s", temp_lists)
  
          # effective_temps dict: `DeviceType: effective temperature`
          effective_temps: Dict[Device.DeviceType, float] = \
              {dev_type: 0 for dev_type in Device.DeviceType}
          for dev_type, temp_list in temp_lists.items():
              if temp_list:
                  effective_temps[dev_type] = max(temp_list)
          logger.info("Effective temperatures are: %s", effective_temps)
  
          # pwm_list: list of effective PWM values
          pwm_list = []
          for dev_type, effective_temp in effective_temps.items():
              pwm_list.append(max(
                  (
                      speed
                      for temp, speed in self.__temperatures[dev_type].items()
                      if effective_temp >= temp
                  ), default=self.__temperatures[dev_type][0]
              ))
          logger.debug("Effective PWM values are: %s", pwm_list)
  
          # Get final PWM speed
          self.__pwm = max(pwm_list)
          logger.info("PWM speed for shelf %s is %s", self, self.__pwm)
  
  
  def add_shelf(shelf: Shelf) -> None:
      """Add a Shelf to the dictionnary of known shelves
  
      :param shelf: Shelf to add
      """
      __SHELVES__[shelf.identifier] = shelf
  
  
  def _handle_ping(client_socket: socket.socket) -> None:
      """Handle the ping request from a client"""
      logger.info("Received REQ_PING from %s", client_socket)
      com.send(client_socket, com.Request.ACK)
  
  
  def _handle_get_pwm(client_socket: socket.socket, shelf_id: str) -> None:
      """Handle the request to get PWM speed of a shelf"""
      logger.info("Received REQ_GET_PWM from %s for %s", client_socket, shelf_id)
      try:
          pwm = __SHELVES__[shelf_id].pwm
      except KeyError as error:
          raise ShelfNotFoundError(f"Shelf {shelf_id} not found") from error
      com.send(client_socket, com.Request.SET_PWM, shelf_id, pwm)
  
  
  def _handle_get_rpm(client_socket: socket.socket, shelf_id: str) -> None:
      """Handle the request to get RPM speed of a shelf"""
      logger.info("Received REQ_GET_RPN from %s for %s", client_socket, shelf_id)
      try:
          rpm = __SHELVES__[shelf_id].rpm
      except KeyError as error:
          raise ShelfNotFoundError(f"Shelf {shelf_id} not found") from error
      com.send(client_socket, com.Request.SET_RPM, shelf_id, rpm)
  
  
  def _handle_set_rpm(client_socket: socket.socket, shelf_id: str,
                      speed: float) -> None:
      """Handle the request to set RPM speef of a shelf"""
      logger.info("Received REQ_SET_RPM to %s from %s for %s",
                  speed, client_socket, shelf_id)
      try:
          __SHELVES__[shelf_id].rpm = speed
      except KeyError as error:
          raise ShelfNotFoundError(f"Shelf {shelf_id} not found") from error
      except ValueError:
          logger.error("Wrong speed value %s received", speed)
          raise
      com.send(client_socket, com.Request.ACK)
  
  
  def _handle_set_pwm_override(client_socket: socket.socket, shelf_id: str,
                               speed: float) -> None:
      """Handle the request to override PWM value of a shelf"""
      logger.info("Received request to override PWM to %s%% from %s for %s",
                  speed, client_socket, shelf_id)
      try:
          __SHELVES__[shelf_id].pwm = speed
      except KeyError as error:
          raise ShelfNotFoundError(f"Shelf {shelf_id} not found") from error
      except ValueError:
          logger.error("Wrong value %s received", speed)
          raise
      com.send(client_socket, com.Request.ACK)
  
  
  def _handle_set_pwm_expire(client_socket: socket.socket, shelf_id: str,
                             date: datetime.datetime) -> None:
      """Handle the request to set the expiration date of the PWM override of
      a shelf
      """
      logger.info("Received set PWM override expiration of %s to %s from %s",
                  shelf_id, date, client_socket)
      try:
          __SHELVES__[shelf_id].pwm_expire = date
      except KeyError as error:
          raise ShelfNotFoundError(f"Shelf {shelf_id} not found") from error
      except ValueError:
          logger.error("Wrong value %s received", date)
          raise
      com.send(client_socket, com.Request.ACK)
  
  
  #: Dictionnary assigning a Request to a function
  REQUEST_HANDLERS: Dict[com.Request, Callable] = {
      com.Request.PING: _handle_ping,
      com.Request.GET_PWM: _handle_get_pwm,
      com.Request.GET_RPM: _handle_get_rpm,
      com.Request.SET_RPM: _handle_set_rpm,
      com.Request.SET_PWM_OVERRIDE: _handle_set_pwm_override,
      com.Request.SET_PWM_EXPIRE: _handle_set_pwm_expire,
  }
  
  
  def listen_client(client_socket: socket.socket) -> None:
      """Listen for client requests until the connection is closed
  
      :param client_socket: Socket to listen to
      """
      logger.info("Listening to %s", client_socket)
      while com.is_socket_open(client_socket):
          try:
              req, args = com.recv(client_socket)
          except TimeoutError:
              logger.warning("%s timed out", client_socket)
              continue
          except ConnectionResetError:
              logger.info("Connection reset by %s", client_socket)
              com.reset_connection(client_socket, notice=False)
              continue
          except CommunicationError:
              logger.exception("Connection error from %s", client_socket)
              com.reset_connection(client_socket)
              continue
          handler = REQUEST_HANDLERS.get(req)
          if handler is None:
              logger.error("Invalid request %s from %s", req, client_socket)
              com.reset_connection(client_socket, "Invalid request")
              continue
          try:
              handler(client_socket, *args)
          except TypeError:
              logger.exception("Invalid call to request %s from %s",
                               req, client_socket)
              com.reset_connection(client_socket, "Invalid call")
              continue
          except ConnectionResetError:
              logger.info("Connection reset by %s", client_socket)
              com.reset_connection(client_socket, notice=False)
              continue
          except TimeoutError:
              logger.exception("%s timed out", client_socket)
              com.reset_connection(client_socket)
              continue
          except CommunicationError:
              logger.exception("Connection error from %s", client_socket)
              com.reset_connection(client_socket)
              continue
          except ShelfNotFoundError as error:
              logger.exception("Shelf not found for %s", client_socket)
              com.reset_connection(client_socket, str(error))
              continue
          except ValueError as error:
              logger.exception("Unexpected value from %s", client_socket)
              com.reset_connection(client_socket, str(error))
              continue
      logger.debug("Stopping client thread for %s: socket closed", client_socket)
  
  
  def _find_config_file() -> Optional[str]:
      """Find the configuration file to use
      Use in order:
        FAND_CONFIG environment variable
        ./fand.ini
        /etc/fand.ini
      """
      if os.environ.get('FAND_CONFIG') is not None:
          return os.environ['FAND_CONFIG']
      if os.path.isfile('./fand.ini'):
          return './fand.ini'
      if os.path.isfile('/etc/fand.ini'):
          return '/etc/fand.ini'
      return None
  
  
  def _read_config_temps(config_string: str) -> Dict[float, float]:
      """Parse a configuration string for a temperature dictionnary"""
      return {
          float(temp.split(':')[0].strip()): float(temp.split(':')[1].strip())
          for temp in config_string.split(',')
      }
  
  
  def read_config(config_file: Optional[str] = _find_config_file()) \
          -> Iterable[Shelf]:
      """Read configuration from a file, returns an iterable of shelves
  
      :param config_file: Configuration file to use, defaults to
          the ``FAND_CONFIG`` environment variable
          or ``./fand.ini`` or ``/etc/fand.ini``
  
      :raises ServerNoConfigError: Configuration not found
      """
      logger.debug("Reading configuration %s", config_file)
      if config_file is None:
          raise ServerNoConfigError("No configuration file found")
      config = configparser.ConfigParser()
      config.read(config_file)
      shelves = set()
      for shelf_id in config['DEFAULT']['shelves'].strip().split(','):
          shelf_id = shelf_id.strip()
          logger.debug("Parsing shelf %s", shelf_id)
          devices = []
          for device_string in config[shelf_id]['devices'].strip().split('\n'):
              serial, position, *_ = device_string.split(';')
              devices.append(Device(serial.strip(), position.strip()))
          hdd_temps = _read_config_temps(config[shelf_id]['hdd_temps'])
          ssd_temps = _read_config_temps(config[shelf_id]['ssd_temps'])
          cpu_temps = _read_config_temps(config[shelf_id]['cpu_temps'])
          shelves.add(Shelf(shelf_id, devices, hdd_temps=hdd_temps,
                            ssd_temps=ssd_temps, cpu_temps=cpu_temps))
      return shelves
  
  
  def shelf_thread(shelf: Shelf) -> None:
      """Monitor a shelf
  
      Stops when :func:`fand.util.terminating()` is True or when an unexpected
      exception occur.
  
      :param shelf: Shelf to monitor
      """
      logger.info("Monitoring %s", shelf)
      while not util.terminating():
          shelf.update()
          util.sleep(shelf.sleep_time)
  
  
  def main() -> NoReturn:
      """Entry point of the module"""
      parser = argparse.ArgumentParser(
          description=__DOCSTRING__,
          formatter_class=argparse.RawDescriptionHelpFormatter,
      )
      parser.add_argument('--config', '-c', default=_find_config_file(),
                          help="Config file to parse")
      args = util.parse_args(parser)
      logger.info("Started from main entry point with parameters %s", args)
      try:
          daemon(config_file=args.config, address=args.address, port=args.port)
      finally:
          util.sys_exit()
  
  
  def daemon(
          config_file: Optional[str] = _find_config_file(),
          address: str = socket.gethostname(),
          port: int = 9999,
  ) -> None:
      """Main function
  
      :param config_file: Configuration file to use, defaults to
          the ``FAND_CONFIG`` environment variable
          or ``./fand.ini`` or ``/etc/fand.ini``
      :param address: Address of the interface to listen on, defaults to hostname
      :param port: Port to listen on
  
      :raises ListeningError: Error while listening for new connections
      """
      logger.debug("Starting server daemon")
      signal.signal(signal.SIGINT, util.default_signal_handler)
      signal.signal(signal.SIGTERM, util.default_signal_handler)
      try:
          for shelf in read_config(config_file):
              add_shelf(shelf)
      except ServerNoConfigError:
          logger.exception("Error while reading config file %s", config_file)
          util.terminate("Cannot continue without configuration")
          raise
  
      with concurrent.futures.ThreadPoolExecutor() as executor:
          def shelf_thread_callback(future: concurrent.futures.Future) -> None:
              """Callback when shelf_thread future is done"""
              error = future.exception()
              if error is not None:
                  util.terminate(f"Exception {error} in shelf thread")
  
          logger.info("Starting shelves threads")
          shelves = []
          for shelf in __SHELVES__.values():
              thread = executor.submit(shelf_thread, shelf)
              thread.add_done_callback(shelf_thread_callback)
              shelves.append(thread)
  
          logger.info("Listening for clients on %s:%s", address, port)
          clients = []
          listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
          try:
              listen_socket.bind((address, port))
              listen_socket.listen()
          except OSError as error:
              logger.exception("Failed to bind to %s:%s", address, port)
              util.terminate("Cannot bind to requested interface and port")
              raise ListeningError(
                  f"Cannot bind to requested interface {address}:{port}"
              ) from error
          com.add_socket(listen_socket)
          while com.is_socket_open(listen_socket):
              try:
                  client_socket, client_address = listen_socket.accept()
                  logger.info("New connection from %s", client_address)
                  com.add_socket(client_socket)
                  clients.append(executor.submit(listen_client, client_socket))
              except OSError as error:
                  if not util.terminating():
                      util.terminate("Error while listening for clients")
                      raise ListeningError(
                          "Error while listening for clients"
                      ) from error
  
  
  if __name__ == '__main__':
      main()
  