class Bnet(bnet_base.BnetBase):

    def __init__(self, 
                 logger: Logger, 
                 ini_file: str = None, 
                 config_file: str = None,
                 debug: bool = False
                 ) -> None:
        path = os.path.dirname(bnet_base.__file__)
        self.ini_file = path + '/' + ini_file
        self.config_file = path + '/' + config_file
        self.debug = debug
        self.logger = logger
        self.logger.debug(f"init: bacpypes with {self.ini_file}")

        cmd_line_args_simulated = []
        if self.debug:
            cmd_line_args_simulated.append("--debug")
            cmd_line_args_simulated.append(__name__)
        cmd_line_args_simulated.append("--ini")
        cmd_line_args_simulated.append(self.ini_file)
        self.args = ConfigArgumentParser().parse_args(cmd_line_args_simulated)

        # load our reliable config file that has ports, names, types, etc.
        self.bacnet_config = json.load(open(self.config_file, 'r'))
        self.logger.debug(f"loaded {self.bacnet_config.get('name')}")
        
        # make a device object
        self.device = LocalDeviceObject(ini=self.args.ini) 

        # Overwrite the IP address in the .ini file to match what this machine
        # is configured for (to work, it must be a static IP on the same
        # subnet as the reliable / bacnet devices: 192.168.1.XXX/24
        self.logger.debug(f"init: this host config IP {self.args.ini.address}")

        # make an application to get callbacks from bacpypes
        self.app = BIPSimpleApplication(self.device, self.args.ini.address)
        enable_sleeping() # backpypes core threads: must do since django is multi threaded
        self.taskman = TaskManager()


    def setup(self) -> None:
        self.logger.debug("setup")


    def reset(self) -> None:
        self.logger.debug("resetting...")


    # Get one of our bacnet config objs from out config dict
    def __get_object(self, obj_id: str) -> Dict:
        for obj in self.bacnet_config.get('objects'):
            if obj.get('id') == obj_id:
                return obj
        return {}

    def __get_device(self) -> str:
        return self.bacnet_config.get('config').get('device_master')

    def __get_prop(self) -> str:
        return self.bacnet_config.get('config').get('object_value_prop')

    # Ping all bacnet devices and write them to stdout
    def ping(self) -> None:
        try:
            # build a request
            request = WhoIsRequest() 
            # ping all devices on network
            request.pduDestination = GlobalBroadcast() 
            # make an IOCB (input output callback)
            iocb = IOCB(request)
            self.app.request_io(iocb)
            self.logger.debug("ping: waiting for responses...")
            loopCount = 0
            while loopCount < 20 and not iocb.ioResponse:
                loopCount += 1
                run_once()
                asyncore.loop(timeout=0.2, count=1)
                time.sleep(0.2)
                self.logger.debug(f"ping: loopy {loopCount}")
            stop()

            # handle responses
            if iocb.ioResponse:
                self.logger.debug(f"ping: iocb response success!")
                apdu = iocb.ioResponse
                if not isinstance(apdu, IAmRequest):
                    self.logger.error(f"ping: Not an IAmRequest")
                    return
                device_type, device_instance = apdu.iAmDeviceIdentifier
                if device_type != 'device':
                    raise DecodingError("ping: invalid object type")
                self.logger.info(f"ping: pduSource={repr(apdu.pduSource)}")
                self.logger.info(f"ping: deviceId={str(apdu.iAmDeviceIdentifier)}")

            # do something for error/reject/abort
            if iocb.ioError:
                self.logger.error(f"ping: {str(iocb.ioError)}")

        except Exception as err:
            exc_type, exc_value, exc_traceback = sys.exc_info()
            traceback.print_tb(exc_traceback, file=sys.stdout)
            self.logger.critical(f"ping: {err}")


    # Helper to read a value
    def __read_value(self, config_obj_id: str) -> float:
        try:
            # get config obj
            obj = self.__get_object(config_obj_id)
            obj_id = obj.get('object_id')
            obj_id = ObjectIdentifier(obj_id).value # make a bacpypes obj id
            addr = self.__get_device()
            prop_id = self.__get_prop()

            # read <addr> <objid> <prop> 
            self.logger.debug(f"read: {config_obj_id} for port \'{obj.get('name')}\' {addr} {str(obj_id)} {prop_id}")

            request = ReadPropertyRequest(
                objectIdentifier=obj_id,
                propertyIdentifier=prop_id
                )
            request.pduDestination = Address(addr)

            iocb = IOCB(request)
            self.app.request_io(iocb)
            self.logger.debug("read: waiting for response...")
            loopCount = 0
            while loopCount < 20 and not iocb.ioResponse:
                loopCount += 1
                run_once()
                asyncore.loop(timeout=0.2, count=1)
                time.sleep(0.2)
                self.logger.debug(f"read: loopy {loopCount}")
            stop()

            # do something for success
            if iocb.ioResponse:
                self.logger.debug(f"read: iocb response success!")
                apdu = iocb.ioResponse

                # should be an ack
                if not isinstance(apdu, ReadPropertyACK):
                    self.logger.error(f"read: response: Not an ACK")
                    return 0

                # find the datatype
                datatype = get_datatype(apdu.objectIdentifier[0], 
                        apdu.propertyIdentifier)
                self.logger.debug(f"read: datatype {datatype}")
                if not datatype:
                    self.logger.error(f"read: unknown datatype")
                    return

                value = apdu.propertyValue.cast_out(datatype)
                self.logger.debug(f"read: value {value}")

                if hasattr(value, 'debug_contents'):
                    value.debug_contents(file=sys.stdout)
                    sys.stdout.flush()

                return value

            # do something for error/reject/abort
            if iocb.ioError:
                self.logger.error(f"read: ioError {str(iocb.ioError)}")

        except Exception as err:
            exc_type, exc_value, exc_traceback = sys.exc_info()
            traceback.print_tb(exc_traceback, file=sys.stdout)
            self.logger.critical(f"read: {err}")
        return None


    # Helper to write a value
    def __write_value(self, config_obj_id: str, _value: float) -> None:
        try:
            # get config obj
            obj = self.__get_object(config_obj_id)
            obj_id = obj.get('object_id')
            obj_id = ObjectIdentifier(obj_id).value # make a bacpypes obj id
            addr = self.__get_device()
            prop_id = self.__get_prop()

            # write <addr> <objid> <prop> <value> 
            value = float(_value)
            self.logger.debug(f"write: {config_obj_id} {_value} for port \'{obj.get('name')}\' {str(obj_id)} {prop_id} {value}")

            request = WritePropertyRequest(
                objectIdentifier=obj_id,
                propertyIdentifier=prop_id
                )
            request.pduDestination = Address(addr)

            # the value to write 
            datatype = get_datatype(obj_id[0], prop_id)
            value = datatype(value)
            request.propertyValue = Any()
            try:
                request.propertyValue.cast_in(value)
            except Exception as err:
                self.logger.critical(f"write: {err}")

            iocb = IOCB(request)
            self.app.request_io(iocb)
            self.logger.debug("write: waiting for response...")
            loopCount = 0
            while loopCount < 20 and not iocb.ioResponse:
                loopCount += 1
                run_once()
                asyncore.loop(timeout=0.2, count=1)
                time.sleep(0.2)
                self.logger.debug(f"write: loopy {loopCount}")
            stop()

            # do something for success
            if iocb.ioResponse:
                self.logger.debug(f"write: iocb response success!")

                apdu = iocb.ioResponse

                # should be an ack
                if not isinstance(iocb.ioResponse, SimpleAckPDU):
                    self.logger.error(f"write: Not an ACK")
                    return
                self.logger.debug(f"write: received ACK")

            # do something for error/reject/abort
            if iocb.ioError:
                self.logger.error(f"write: ioError {str(iocb.ioError)}")

        except Exception as err:
            exc_type, exc_value, exc_traceback = sys.exc_info()
            traceback.print_tb(exc_traceback, file=sys.stdout)
            self.logger.critical(f"write: {err}")


    def set_test_voltage(self, voltage: float) -> None:
        self.logger.info(f"set_test_voltage {voltage}")
        self.__write_value('test_v', voltage)


    def set_air_temp(self, tempC: float) -> None:
        # convert C to F
        # tempF = (tempC * (9/5)) + 32
        # self.logger.info(f"set_air_temp {tempC}C > {tempF}F")
        self.logger.info(f"set_air_temp {tempC}C")
        self.__write_value('set_air_temp', tempC) #self.__write_value('set_air_temp', tempF)


    def set_air_RH(self, RH: float) -> None:
        self.logger.info(f"set_air_RH {RH}")
        self.__write_value('set_air_rh', RH)


    def get_air_temp(self) -> float:
        #tempF = self.__read_value('air_temp')
        tempC = self.__read_value('air_temp')
        #if tempF is None:
        #    return None
        # convert F to C
        #tempC = (tempF - 32) * (5/9)
        self.logger.info(f"get_air_temp {tempC}C")
        return tempC


    def get_air_RH(self) -> float:
        RH = self.__read_value('air_rh')
        self.logger.info(f"get_air_RH {RH}%")
        return RH