#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#
#   This module handles all tracking activities for a device. It contains
#   the following modules:
#       TrackFromZones - iCloud3 creates an object for each device/zone
#           with the tracking data fields.
#
#   The primary methods are:
#       determine_interval - Determines the polling interval, update times,
#           location data, etc for the device based on the distance from
#           the zone.
#       determine_interval_after_error - Determines the interval when the
#           location data is to be discarded due to poor GPS, it is old or
#           some other error occurs.
#
#
#
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

from ..global_variables     import GlobalVariables as Gb
from ..const                import (HOME, NOT_HOME, AWAY, NOT_SET, HIGH_INTEGER,
                                    CRLF, CHECK_MARK, CIRCLE_X, LTE, LT, PLUS_MINUS, RED_X, CIRCLE_STAR2,
                                    STATIONARY, STATIONARY_FNAME, WATCH, IOSAPP_FNAME,
                                    AWAY_FROM, TOWARDS, PAUSED, INZONE, NEAR, NEAR_HOME,
                                    ERROR, UNKNOWN,
                                    VALID_DATA,
                                    WAZE,
                                    NEAR_DEVICE_DISTANCE,
                                    WAZE_USED, WAZE_NOT_USED, WAZE_PAUSED, WAZE_OUT_OF_RANGE, WAZE_NO_DATA,
                                    OLD_LOC_POOR_GPS_CNT, AUTH_ERROR_CNT, RETRY_INTERVAL_RANGE_1, IOSAPP_REQUEST_LOC_CNT,
                                    RETRY_INTERVAL_RANGE_2,
                                    EVLOG_TIME_RECD, EVLOG_ALERT,
                                    RARROW, NEAR_DEVICE_USEABLE_SYM,
                                    EXIT_ZONE,
                                    ZONE, ZONE_INFO, INTERVAL,
                                    DISTANCE, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE,
                                    MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD,
                                    TRAVEL_TIME, TRAVEL_TIME_MIN, TRAVEL_TIME_HHMM, ARRIVAL_TIME, DIR_OF_TRAVEL, MOVED_DISTANCE,
                                    LAST_LOCATED, LAST_LOCATED_TIME, LAST_LOCATED_DATETIME,
                                    LAST_UPDATE, LAST_UPDATE_TIME, LAST_UPDATE_DATETIME,
                                    NEXT_UPDATE, NEXT_UPDATE_TIME, NEXT_UPDATE_DATETIME,
                                    LAST_LOCATED,
                                    )

from ..support              import iosapp_interface
from ..support              import stationary_zone as statzone
from ..helpers.common       import (instr, round_to_zero, is_zone, is_statzone, isnot_zone,
                                    zone_display_as, )
from ..helpers.messaging    import (post_event, post_error_msg,
                                    post_internal_error, post_monitor_msg, log_debug_msg, log_rawdata,
                                    log_info_msg, log_error_msg, log_exception, _trace, _traceha, )
from ..helpers.time_util    import (secs_to_time, secs_to_time_str, secs_to_time_age_str, waze_mins_to_time_str,
                                    secs_since, time_to_12hrtime, secs_to_datetime, secs_to, secs_to_age_str,
                                    datetime_now, time_now, time_now_secs, secs_to_time_hhmm, secs_to_hhmm, )
from ..helpers.dist_util    import (km_to_mi, km_to_mi_str, format_dist_km,  format_dist_m, format_km_to_mi, )

import homeassistant.util.dt as dt_util
import traceback

# location_data fields
LD_STATUS      = 0
LD_ZONE_DIST   = 1
LD_ZONE_DIST_M = 2
LD_WAZE_DIST   = 3
LD_CALC_DIST   = 4
LD_WAZE_TIME   = 5
LD_MOVED       = 6
LD_DIRECTION   = 7

#waze_from_zone fields
WAZ_STATUS   = 0
WAZ_TIME     = 1
WAZ_DISTANCE = 2
WAZ_MOVED    = 3

#<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
def determine_interval(Device, FromZone):
    '''
    Calculate new interval. Return location based attributes.
    The durrent location:   Device.loc_data_latitude/longitude.
    The last location:      Device.sensors[LATITUDE]/[LONGITUDE]
    '''

    devicename = Device.devicename

    battery10_flag       = (0 > Device.dev_data_battery_level >= 10)
    battery5_flag        = (0 > Device.dev_data_battery_level >= 5)

    inzone_flag          = (Device.loc_data_zone != NOT_HOME)
    not_inzone_flag      = (Device.loc_data_zone == NOT_HOME)
    was_inzone_flag      = (Device.sensors[ZONE] not in [NOT_HOME, AWAY, NOT_SET])

    inzone_home_flag     = (Device.loc_data_zone == HOME)
    was_inzone_home_flag = (Device.sensor_zone == HOME)

    Device.FromZone_BeingUpdated = FromZone

    if FromZone.from_zone == Device.loc_data_zone:
        Device.FromZone_LastIn = FromZone

    if Device.offline_secs > 0:
        event_msg =(f"{EVLOG_ALERT}Device back Online > "
                    f"WentOfflineAt-{secs_to_time_age_str(Device.offline_secs)}, "
                    f"DeviceStatus-{Device.device_status}")
        post_event(devicename, event_msg)
        Device.offline_secs = 0

    #--------------------------------------------------------------------------------
    Device.write_ha_sensor_state(LAST_LOCATED, Device.loc_data_time)

    if used_near_device_results(Device, FromZone):
        return FromZone.sensors

    Device.NearDeviceUsed = None

    #--------------------------------------------------------------------------------
    location_data = _get_distance_data(Device, FromZone)

    # If an error occurred, the [1] entry contains an attribute that can be passed to
    # display_info_code to display an error message
    if (location_data[LD_STATUS] == ERROR):
        return location_data[LD_ZONE_DIST]

    dist_from_zone_km       = location_data[LD_ZONE_DIST]
    dist_from_zone_m        = location_data[LD_ZONE_DIST_M]
    waze_dist_from_zone_km  = location_data[LD_WAZE_DIST]
    calc_dist_from_zone_km  = location_data[LD_CALC_DIST]
    waze_time_from_zone     = location_data[LD_WAZE_TIME]
    dist_moved_km           = location_data[LD_MOVED]
    dir_of_travel           = location_data[LD_DIRECTION]

    log_msg = ( f"DistFmZome-{dist_from_zone_km}, Moved-{dist_moved_km}, "
                f"Waze-{waze_dist_from_zone_km}, Calc-{calc_dist_from_zone_km}, "
                f"TravTime-{waze_time_from_zone}, Dir-{dir_of_travel}, "
                f"DirHist-{FromZone.dir_of_travel_history}")
    log_debug_msg(devicename, log_msg)

    #--------------------------------------------------------------------------------
    # The following checks the distance from home and assigns a
    # polling interval in minutes.  It assumes a varying speed and
    # is generally set so it will poll one or twice for each distance
    # group. When it gets real close to home, it switches to once
    # each 15 seconds so the distance from home will be calculated
    # more often and can then be used for triggering automations
    # when you are real close to home. When home is reached,
    # the distance will be 0.

    Device.display_info_msg( f"Determine Interval-{FromZone.info_status_msg}")

    # Reset got zone exit trigger since now in a zone for next
    # exit distance check. Also reset Stat Zone timer and dist moved.
    if inzone_flag:
        Device.got_exit_trigger_flag = False
        Device.statzone_clear_timer

    waze_time_msg = '0 min'
    calc_interval_secs = round(km_to_mi(dist_from_zone_km) / 1.5) * 60
    if Gb.Waze.is_status_USED:
        waze_interval_secs = round(waze_time_from_zone * 60 * Gb.travel_time_factor , 0)
    else:
        waze_interval_secs = 0

    #--------------------------------------------------------------------------------
    #if more than 3km(1.8mi) then assume driving
    last_went_3km = Device.went_3km
    if FromZone is Device.FromZone_LastIn:
        if dist_from_zone_km > 3:
            oldway_went_3km = True
        elif dist_from_zone_km < .03:    # back in the zone, reset flag
            oldway_went_3km = False

    #--------------------------------------------------------------------------------
    #if more than 3km(1.8mi) then assume driving
    if FromZone is Device.FromZone_Home:     #Device.FromZone_LastIn:
        if dist_from_zone_km > 3:
            Device.went_3km = True
        elif dist_from_zone_km < .03:    # back in the zone, reset flag
            Device.went_3km = False

    #--------------------------------------------------------------------------------
    interval_secs   = 15
    interval_str    = ''
    interval_method = ''
    interval_multiplier = 1

    if Device.state_change_flag:
        if inzone_flag:
            #inzone & old location
            if Device.is_location_old_or_gps_poor and battery10_flag is False:
                interval_method = '1.OldLocPoorGPS'
                interval_secs   = _get_interval_for_error_retry_cnt(Device, OLD_LOC_POOR_GPS_CNT)

            elif Device.isnot_in_statzone:
                interval_method = "1.EnterZone"
                interval_secs   = Device.inzone_interval_secs

        #battery < 5% and near zone
        elif battery5_flag and dist_from_zone_km <= 1:
            interval_method = "2.Battery5%"
            interval_secs   = 15

        #battery < 10%
        elif battery10_flag:
            interval_method = "2.Battery10%"
            interval_secs   = Device.statzone_inzone_interval_secs

        #exited zone, set to short interval if other devices are in same zone
        elif not_inzone_flag and was_inzone_flag:
            interval_method = "2.ExitZone"
            interval_secs   = Gb.exit_zone_interval_secs

            if Device.loc_data_zone == HOME:
                FromZone.max_dist_km = 0
        else:
            interval_method = "2.ZoneChanged"
            interval_secs   = 240

    # Exit_Zone trigger & away & exited less than 1 min ago
    elif (instr(Device.trigger, EXIT_ZONE)
            and not_inzone_flag
            and secs_since(Device.iosapp_zone_exit_secs) < 60):
        interval_method = '3.ExitTrigger'
        interval_secs   = Gb.exit_zone_interval_secs

    #inzone & poor gps & check gps accuracy when inzone
    elif (Device.is_gps_poor
            and inzone_flag
            and Gb.discard_poor_gps_inzone_flag is False):
        interval_method = '3.PoorGPSinZone'
        interval_secs   = _get_interval_for_error_retry_cnt(Device, OLD_LOC_POOR_GPS_CNT)

    elif Device.is_gps_poor:
        interval_method = '3.PoorGPS'
        interval_secs   = _get_interval_for_error_retry_cnt(Device, OLD_LOC_POOR_GPS_CNT)

    elif Device.is_location_old_or_gps_poor:
        interval_method = '3.OldLocPoorGPS'
        interval_secs   = _get_interval_for_error_retry_cnt(Device, OLD_LOC_POOR_GPS_CNT)

    # elif Device.is_in_statzone:
    #    interval_method = "3.StatZone"
    #    interval_secs   = Device.statzone_inzone_interval_secs

    elif battery10_flag and dist_from_zone_km > 1:
        interval_method = "3.Battery10%"
        interval_secs   = Device.statzone_inzone_interval_secs

    elif inzone_home_flag or (dist_from_zone_km < .05 and dir_of_travel == TOWARDS):
        interval_method = '3.InHomeZone'
        interval_secs   = Device.inzone_interval_secs

    #in another zone and inzone time > travel time
    elif inzone_flag and Device.inzone_interval_secs > waze_interval_secs:
        interval_method = '3.InZone'
        interval_secs   = Device.inzone_interval_secs

    elif dir_of_travel ==  NOT_SET:
        interval_method = '3.NeedInfo'
        interval_secs   = 150

    elif dist_from_zone_km < 2 and dir_of_travel == AWAY_FROM:
        interval_method = '<2km.Away'
        interval_secs   = Device.old_loc_threshold_secs  #1.5 mi & going Away

    elif dist_from_zone_km < 2 and Device.went_3km:
        interval_method = '<2km'
        interval_secs   = 15          #1.5 mi = real close and driving

    elif dist_from_zone_km < 2:       #1.5 mi=1 min
        interval_method = '<3km'
        interval_secs   = 60

    elif dist_from_zone_km < 3.5:      #2 mi=1.5 min
        interval_method = '<3.5km'
        interval_secs   = 90

    elif waze_time_from_zone > 5 and waze_interval_secs > 0:
        interval_method = '3.WazeTime'
        interval_secs   = waze_interval_secs

    elif dist_from_zone_km < 5:        #3 mi=2 min
        interval_method = '<5km'
        interval_secs   = 120

    elif dist_from_zone_km < 8:        #5 mi=3 min
        interval_method = '<8km'
        interval_secs   = 180

    elif dist_from_zone_km < 12:       #7.5 mi=5 min
        interval_method = '<12km'
        interval_secs   = 300

    elif dist_from_zone_km < 20:       #12 mi=10 min
        interval_method = '<20km'
        interval_secs   = 600

    elif dist_from_zone_km < 40:       #25 mi=15 min
        interval_method = '<40km'
        interval_secs   = 900

    elif dist_from_zone_km > 150:      #90 mi=1 hr
        interval_method = '>150km'
        interval_secs   = 3600

    else:
        interval_method = '3.Calculated'
        interval_secs   = calc_interval_secs

    if (dir_of_travel in ('', ' ', '___', AWAY_FROM)
            and interval_secs < 180
            and interval_secs > 30):
        interval_method += '+6.Away(<3min)'
        interval_secs = 180

    elif dir_of_travel == AWAY_FROM and not Gb.Waze.distance_method_waze_flag:
        interval_method += '+6.Away(Calc)'
        interval_multiplier = 2    #calc-increase timer

    elif dir_of_travel == NOT_SET and interval_secs > 180:
        interval_method += '+>180s'
        interval_secs    = 180

    #15-sec interval_secs (close to zone) and may be going into a stationary zone,
    #increase the interval
    # elif interval_secs == 15 and Gb.this_update_secs >= Device.statzone_timer+45:
    #     interval_method += '+6.StatTimer+45'
    #     interval_secs = 30

    #Turn off waze close to zone flag to use waze after leaving zone or getting more than 1km from it
    if Gb.Waze.waze_close_to_zone_pause_flag:
        if inzone_flag or calc_dist_from_zone_km >= 1:
            Gb.Waze.waze_close_to_zone_pause_flag = False

    #if triggered by ios app (Zone Enter/Exit, Manual, Fetch, etc.)
    if (Device.iosapp_update_flag
            and interval_secs < 180
            and interval_secs > 30):
        interval_method += '+7.iosAppTrigger'
        interval_secs    = 180

    #if changed zones on this poll reset multiplier
    if Device.state_change_flag:
        interval_multiplier = 1

    #Check accuracy again to make sure nothing changed, update counter
    if Device.is_gps_poor:
        interval_multiplier = 1

    try:
        #Real close, final check to make sure interval_secs is not adjusted
        if (interval_secs <= 60
                or ((0 > Device.dev_data_battery_level >= 33) and interval_secs >= 120)):
            interval_multiplier = 1

        interval_secs     = interval_secs * interval_multiplier
        interval_secs, x  = divmod(interval_secs, 5)
        interval_secs     = interval_secs * 5

        #  Use interval_secs if > StatZone interval unless the StatZone interval is the device's
        # inzone interval
        if Device.is_in_statzone:
                #and Device.statzone_inzone_interval_secs >= interval_secs):
            interval_method = "7.StatZone"
            interval_secs   = Device.statzone_inzone_interval_secs

        #check for max interval_secs, override in zone times
        elif interval_secs > Gb.max_interval_secs:
            if Device.is_in_statzone:
                interval_method = f"7.inZoneMax"
                interval_secs   = Device.statzone_inzone_interval_secs
            elif inzone_flag:
                pass
                # interval_method = f"7.inZoneMax"
                # interval_secs   = Gb.max_interval_secs / 2
            else:
                interval_method = f"7.Max"
                interval_secs   = Gb.max_interval_secs

        interval_str = secs_to_time_str(interval_secs)

        if interval_multiplier > 1:
            interval_method += (f"x{interval_multiplier}")

        #check if next update is past midnight (next day), if so, adjust it
        next_update_secs = round((Gb.this_update_secs + interval_secs)/5, 0) * 5

        # If the device is monitored, the get the smallest next_update_secs for the tracked devices.
        # Override the results of this device with the next one to be updated.
        try:
            if Device.is_monitored:
                for _Device in Gb.Devices_by_devicename_tracked.values():
                    if (_Device.FromZone_TrackFrom
                            and _Device.near_device_distance <= NEAR_DEVICE_DISTANCE
                            and next_update_secs < _Device.FromZone_TrackFrom.next_update_secs):
                        next_update_secs = _Device.FromZone_TrackFrom.next_update_secs
                        interval_secs    = _Device.FromZone_TrackFrom.interval_secs
                        interval_str     = _Device.FromZone_TrackFrom.interval_str
                        interval_method  = f"{_Device.fname}"
        except:
            error_msg = f"User Monitor Results error > {_Device.devicename}, Zone-{_Device.FromZone_TrackFrom.from_zone}"
            post_error_msg(error_msg)
            pass

        # Update all dates and other fields
        Device.loc_data_dist_moved_km = dist_moved_km
        FromZone.interval_secs    = interval_secs
        FromZone.interval_str     = interval_str
        FromZone.next_update_secs = next_update_secs
        FromZone.next_update_time = secs_to_time(next_update_secs)
        FromZone.last_update_secs = Gb.this_update_secs
        FromZone.last_update_time = time_to_12hrtime(Gb.this_update_time)

        FromZone.interval_method  = interval_method
        FromZone.dir_of_travel    = dir_of_travel
        if FromZone.zone_dist < 100:
            FromZone.dir_of_travel_history += dir_of_travel[0:1]
            monitor_msg = (f"DirHist-{FromZone.format_dir_of_travel_history}")
            post_monitor_msg(devicename, monitor_msg)

        if inzone_home_flag: FromZone.dir_of_travel_history = ''

    except Exception as err:
        sensor_msg = post_internal_error('Update FromZone Times', traceback.format_exc)

    #--------------------------------------------------------------------------------
    # if poor gps and moved less than 1km, redisplay last distances
    if (Device.state_change_flag is False
            and Device.is_gps_poor
            and dist_moved_km < 1):
        dist_from_zone_km      = FromZone.zone_dist
        dist_from_zone_m       = FromZone.zone_dist_m
        waze_dist_from_zone_km = FromZone.waze_dist
        calc_dist_from_zone_km = FromZone.calc_dist
        waze_time_from_zone    = FromZone.waze_time

    else:
        #save for next poll if poor gps
        FromZone.zone_dist   = dist_from_zone_km
        FromZone.zone_dist_m = dist_from_zone_m
        FromZone.waze_dist   = waze_dist_from_zone_km
        FromZone.waze_time   = waze_time_from_zone
        FromZone.calc_dist   = calc_dist_from_zone_km

    # waze_time_msg = Gb.Waze.waze_mins_to_time_str(waze_time_from_zone)
    waze_time_msg = secs_to_time_str(waze_time_from_zone * 60)

    if (Device.is_location_gps_good
            and interval_secs > 60
            and waze_dist_from_zone_km > FromZone.max_dist_km):
        FromZone.max_dist_km = waze_dist_from_zone_km

    #--------------------------------------------------------------------------------
    #Make sure the new 'last state' value is the internal value for
    #the state (e.g., Away-->not_home) to reduce state change triggers later.
    sensors                       = {}
    sensors[LAST_LOCATED_DATETIME]= Device.loc_data_datetime
    sensors[LAST_LOCATED_TIME]    = Device.loc_data_time
    sensors[LAST_LOCATED]         = Device.loc_data_time

    sensors.update(_update_next_update_fields_and_sensors(None, interval_secs))

    sensors[TRAVEL_TIME]          = secs_to_time_str(waze_time_from_zone * 60)
    sensors[TRAVEL_TIME_MIN]      = f"{waze_time_from_zone:.0f} min"
    sensors[TRAVEL_TIME_HHMM]     = secs_to_hhmm(waze_time_from_zone * 60)

    if Device.is_inzone and Device.loc_data_zone == FromZone.from_zone:
        sensors[ARRIVAL_TIME]     =f"@{secs_to_time_hhmm(Device.zone_change_secs)}"
    else:
        sensors[ARRIVAL_TIME]     = secs_to_time_hhmm(waze_time_from_zone * 60 + time_now_secs())
    sensors[DIR_OF_TRAVEL]        = dir_of_travel

    sensors[DISTANCE]             = km_to_mi(dist_from_zone_km)
    sensors[MAX_DISTANCE]         = km_to_mi(FromZone.max_dist_km)
    sensors[ZONE_DISTANCE]        = km_to_mi(dist_from_zone_km)
    sensors[ZONE_DISTANCE_M]      = dist_from_zone_m
    sensors[ZONE_DISTANCE_M_EDGE] = abs(dist_from_zone_m - FromZone.from_zone_radius_m)
    sensors[WAZE_DISTANCE]        = km_to_mi(waze_dist_from_zone_km)
    sensors[WAZE_METHOD]          = Gb.Waze.waze_status_fname
    sensors[CALC_DISTANCE]        = km_to_mi(calc_dist_from_zone_km)
    sensors[MOVED_DISTANCE]       = km_to_mi(dist_moved_km)

    if Device.is_inzone:
        sensors[ZONE_INFO] = f"@{Device.loc_data_zone_fname}"
    else:
        sensors[ZONE_INFO] = km_to_mi_str(dist_from_zone_km)

    #save for event log
    if type(waze_time_msg) != str: waze_time_msg = ''
    FromZone.last_travel_time  = waze_time_msg
    FromZone.last_distance_km  = dist_from_zone_km
    FromZone.last_distance_str = (f"{format_km_to_mi(dist_from_zone_km)}")

    if Device.is_location_gps_good:
        Device.old_loc_poor_gps_cnt = 0

    Device.display_info_msg(Device.format_info_msg, new_base_msg=True)
    post_results_message_to_event_log(Device, FromZone)
    post_zone_time_dist_event_msg(Device, FromZone)
    FromZone.sensors.update(sensors)

    return sensors

#--------------------------------------------------------------------------------
def post_results_message_to_event_log(Device, FromZone):
    '''
    Post the final tracking results to the Event Log and HA log file
    '''
    Device.last_update_msg_secs = time_now_secs()
    event_msg = (f"Results: From-{FromZone.from_zone_display_as} > ")

    if True is True:  #Device.is_tracked:
        event_msg += (  f"NextUpdate-{FromZone.next_update_time}, "
                        f"Interval-{FromZone.interval_str}, ")
    if FromZone.zone_dist > 0:
        event_msg += (  f"TravTime-{FromZone.last_travel_time}, "
                        f"Distance-{format_km_to_mi(FromZone.zone_dist)}, ")
    if FromZone.dir_of_travel == STATIONARY_FNAME:
        event_msg +=    STATIONARY_FNAME + ", "
    elif FromZone.dir_of_travel not in [INZONE, '_', '___', ' ', '']:
        event_msg +=    f"Direction-{FromZone.dir_of_travel}, "
    if Device.is_statzone_timer_set and Device.is_tracked and Gb.is_statzone_used:
        event_msg +=    f"IntoStatZone-{secs_to_time(Device.statzone_timer)}, "
    # if Device.statzone_dist_moved_km > 0:
    #     event_msg +=    f"Moved-{format_dist_km(Device.statzone_dist_moved_km)}, "
    if Device.loc_data_dist_moved_km > 0:
        event_msg +=    f"Moved-{format_dist_km(Device.loc_data_dist_moved_km)}, "
    if Device.dev_data_battery_level > 0:
        event_msg +=    f"Battery-{Device.dev_data_battery_level}%, "
    if Device.is_monitored:
        event_msg +=     f"{Device.dev_data_source}, "
    if Gb.log_debug_flag and FromZone.interval_method and Device.is_tracked:
        event_msg +=    (f"Method-{FromZone.interval_method}, "
                        f"{'Went3km, ' if Device.went_3km else ''}")
    if Gb.Waze.waze_status == WAZE_OUT_OF_RANGE:
        event_msg +=    "Waze-OverMaxDist, "
    post_event(Device.devicename, event_msg[:-2])

    if True is True: #Device.is_tracked:
        log_msg = ( f"RESULTS: From-{FromZone.from_zone_display_as} > "
                    f"iOSAppZone-{Device.iosapp_data_state}, "
                    f"iC3Zone-{Device.loc_data_zone}, "
                    f"Interval-{FromZone.interval_str}, "
                    f"TravTime-{FromZone.last_travel_time}, "
                    f"Dist-{format_km_to_mi(FromZone.zone_dist)}, "
                    f"NextUpdt-{FromZone.next_update_time}, "
                    f"MaxDist-{format_km_to_mi(FromZone.max_dist_km)}, "
                    f"Dir-{FromZone.dir_of_travel}, "
                    f"Moved-{format_dist_km(Device.statzone_dist_moved_km)}, "
                    f"Battery-{Device.dev_data_battery_level}%, "
                    f"LastDataUpdate-{secs_to_time(Device.last_data_update_secs)}, "
                    f"GPSAccuracy-{Device.loc_data_gps_accuracy}m, "
                    f"LocAge-{secs_to_age_str(Device.loc_data_secs)}, "
                    f"OldThreshold-{secs_to_time_str(Device.old_loc_threshold_secs)}, "
                    f"LastEvLogMsg-{secs_to_time(Device.last_evlog_msg_secs)}, "
                    f"Method-{FromZone.interval_method}")
        log_info_msg(Device.devicename, log_msg)

#--------------------------------------------------------------------------------
def post_zone_time_dist_event_msg(Device, FromZone):
    '''
    Post the iosapp state, ic3 zone, interval, travel time, distance msg to the
    Event Log
    '''

    iosapp_state = 'â”€â”€'
    if Device.iosapp_monitor_flag:
        iosapp_state = zone_display_as(Device.iosapp_data_state)
        if (is_statzone(Device.iosapp_data_state)
                and Device.iosapp_data_state != Device.loc_data_zone
                and Device.iosapp_data_state not in statzone.ha_statzones()):
            iosapp_state = f"ðŸ…§{iosapp_state}"

    ic3_zone = zone_display_as(Device.loc_data_zone)

    if Device.loc_data_zone == NOT_SET:
        interval_str = travel_time = distance = 0
    else:
        interval_str = FromZone.interval_str.split("(")[0]
        travel_time  = '' if FromZone.last_travel_time == '0 min' else FromZone.last_travel_time
        distance     = FromZone.zone_distance_str

    event_msg =(f"{EVLOG_TIME_RECD}{iosapp_state},{ic3_zone},{interval_str},{travel_time},{distance}")
    post_event(Device.devicename, event_msg)

#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#
#   iCloud FmF or FamShr authentication returned an error or no location
#   data is available. Update counter and device attributes and set
#   retry intervals based on current retry count.
#
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def determine_interval_monitored_device_offline(Device):
    '''
    Handle errors where the device can not be or should not be updated with
    the current data. The update will be retried 4 times on a 15 sec interval.
    If the error continues, the interval will increased based on the retry
    count using the following cycles:
    '''
    if Device.is_online:
        Device.offline_secs = 0
        return False

    if (Device.is_offline and Device.offline_secs == 0):
        Device.offline_secs = Gb.this_update_secs

    age = secs_since(Device.loc_data_secs)
    if age > 3600:
        interval_secs = 3600        # 1-hr
    elif age > 1800:
        interval_secs = 1800        #30-min
    elif age > 3600:
        interval_secs = 900         #15-min
    else:
        interval_secs = 300         # 5-min

    update_all_device_fm_zone_sensors_interval(Device, interval_secs)

    event_msg =(f"{RED_X}Offline > "
                f"Since-{secs_to_time_age_str(Device.loc_data_secs)}, "
                f"CheckNext-{Device.sensors[NEXT_UPDATE_TIME]}")
    post_event(Device.devicename, event_msg)

    return True

#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#
#   iCloud FmF or FamShr authentication returned an error or no location
#   data is available. Update counter and device attributes and set
#   retry intervals based on current retry count.
#
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def determine_interval_after_error(Device, counter=OLD_LOC_POOR_GPS_CNT):
    '''
    Handle errors where the device can not be or should not be updated with
    the current data. The update will be retried 4 times on a 15 sec interval.
    If the error continues, the interval will increased based on the retry
    count using the following cycles:
        1-4   - 15 sec
        5-8   - 1 min
        9-12  - 5min
        13-16 - 15min
        >16   - 30min

    The following errors use this routine:
        - iCloud Authentication errors
        - FmF location data not available
        - Old location
        - Poor GPS Acuracy
    '''
    devicename = Device.devicename

    try:
        interval_secs, error_cnt, max_error_cnt = get_error_retry_interval(Device, counter)

        # Pause tracking when the max count is exceeded, send paused msg when the cnt is reached
        if error_cnt >= max_error_cnt:
            Device.pause_tracking()

            if error_cnt == max_error_cnt:
                message = {
                    "title": "iCloud3 Tracking Exception",
                    "message": (f"Old Location or Poor GPS Accuracy Error "
                                f"Count exceeded (#{error_cnt}). Event Log > Actions > "
                                f"Resume to restart tracking."),
                    "data": {"subtitle": "Tracking has been Paused"}}
                iosapp_interface.send_message_to_device(Device, message)

        if (Device.is_offline and Device.offline_secs == 0):
            Device.offline_secs = Gb.this_update_secs

        # Often, iCloud does not actually locate the device but just returns the last
        # location it has. A second call is needed after a 5-sec delay. This also
        # happens after a reauthentication. If so, do not display an error on the
        # first retry.

        threshold_msg = ''  if interval_secs == Device.interval_secs \
                            else f", OldThreshold-{secs_to_time_str(Device.old_loc_threshold_secs)}"

        next_update_secs = Gb.this_update_secs + interval_secs
        Device.update_sensors_error_msg = Device.update_sensors_error_msg or Device.old_loc_poor_gps_msg

        update_all_device_fm_zone_sensors_interval(Device, interval_secs)

        Device.display_info_msg(Device.update_sensors_error_msg)
        event_msg = ''

        if (Device.is_data_source_FAMSHR and Device.is_offline):
            event_msg =(f"{EVLOG_ALERT}Device is Offline > "
                        f"WentOfflineAt-{secs_to_time_age_str(Device.offline_secs)}, "
                        f"RetryAt-{secs_to_time(next_update_secs)}, "
                        f"DeviceStatus-{Device.device_status}")

        elif ((Device.old_loc_poor_gps_cnt > 0
                    and secs_since(Device.FromZone_Home.next_update_secs) < \
                        (Device.FromZone_Home.interval_secs + 5))
                or Device.outside_no_exit_trigger_flag):
            pass

        elif counter == AUTH_ERROR_CNT:
            event_msg =(f"Results > RetryCounter-{Gb.icloud_acct_error_cnt}, "
                        f"RetryAt {secs_to_time(next_update_secs)} "
                        f"({Device.FromZone_Home.interval_str})")

        if event_msg == '' and Device.update_sensors_error_msg != '':
            event_msg =(f"{Device.update_sensors_error_msg}, "
                        f"RefreshAt-{secs_to_time(next_update_secs)} "
                        f"({Device.FromZone_Home.interval_str})"
                        f"{threshold_msg}")
            Device.icloud_update_reason = "Newer Data is Available"

        if event_msg and Device.old_loc_poor_gps_cnt > 2:
            post_event(devicename, event_msg)
            log_info_msg(Device.devicename, f"Old Location/Other Error-{event_msg}")
            # log_rawdata(f"{Device_devicename} - {from_zone}", FromZone.sensors)

    except Exception as err:
        log_exception(err)

#----------------------------------------------------------------------------
def get_error_retry_interval(Device, counter=OLD_LOC_POOR_GPS_CNT):
    '''
    Determine the interval value based on the error counter and current retry_count

    Return:
        interval value

    Called from:
        determine interval after error
        Device.post_location_data_accepted_rejected_msg

    '''
    try:
        if Device.is_offline:
            if Device.sensor_zone == NOT_SET:
                return 120, 0, 20
            else:
                return Gb.offline_interval_secs, 0, 20

        interval_secs = 0

        if counter == OLD_LOC_POOR_GPS_CNT:
            error_cnt = Device.old_loc_poor_gps_cnt
            range_tbl = RETRY_INTERVAL_RANGE_1

        elif counter == AUTH_ERROR_CNT:
            error_cnt = Gb.icloud_acct_error_cnt
            range_tbl = RETRY_INTERVAL_RANGE_1

        elif counter == IOSAPP_REQUEST_LOC_CNT:
            error_cnt = Device.iosapp_request_loc_retry_cnt
            range_tbl = RETRY_INTERVAL_RANGE_2
        else:
            error_cnt = Device.old_loc_poor_gps_cnt
            range_tbl = RETRY_INTERVAL_RANGE_1
            interval_secs = 60

        max_error_cnt = int(list(range_tbl.keys())[-1])
        if max_error_cnt < 20: max_error_cnt = 20

        # Retry in 10-secs if this is the first time retried
        if error_cnt <= 1:
            interval_secs = 5

        else:
            interval_list = [cnt_time for cnt, cnt_time in range_tbl.items() if cnt <= error_cnt]
            interval_secs = interval_list[-1]
            interval_secs = interval_secs * 60

    except Exception as err:
        log_exception(err)

    return interval_secs, error_cnt, max_error_cnt

#----------------------------------------------------------------------------
def update_all_device_fm_zone_sensors_interval(Device, interval_secs, FromZone=None):
    '''
    Update the Device and FromZone sensors with a new interval

    Parameters:
        Device - Device to be updated
        interval_secs - New polling interval_secs that determins the next_update_time
        FromZone - Update all FromZones or only this FromZone
    '''

    # if next_update_secs < Device.next_update_secs:
    if FromZone:
        Device.FromZone_NextToUpdate = FromZone

    _update_next_update_fields_and_sensors(Device, interval_secs)

    # Set all track from zone intervals. This prevents one zone from triggering an update
    # when the location data was poor.
    for _FromZone in Device.FromZones_by_zone.values():
        _update_next_update_fields_and_sensors(_FromZone, interval_secs)

        # Move Stationary Zone timer if it is set and expired so it does not trigger an update
        if Device.is_statzone_timer_reached and Gb.is_statzone_used:
            Device.statzone_timer = Device.next_update_secs

#----------------------------------------------------------------------------
def _update_next_update_fields_and_sensors(Device_or_FromZone, interval_secs):
    '''
    Update all of the internal and sensor fields for an interval value.

    Parameters:
        Device   - The Device or FromZone to update
                   None: Update a sensors dictionary to be merged with Device.sensors later
        interval - The new interval (secs)

    Return:
        sensors  - Dictionary of updated sensors that can be merged with the Device's
                    sensors.
    '''
    next_update_secs = Gb.this_update_secs + interval_secs
    next_update_time = secs_to_time(next_update_secs)

    sensors = {}

    if Device_or_FromZone:
        Device = Device_or_FromZone if Device_or_FromZone in Gb.Devices else Device_or_FromZone.Device
        data_source_ICLOUD = Device.is_data_source_ICLOUD
    else:
        data_source_ICLOUD = Gb.primary_data_source_ICLOUD

    sensors[INTERVAL]             = secs_to_time_str(interval_secs)
    sensors[NEXT_UPDATE_DATETIME] = secs_to_datetime(next_update_secs)
    sensors[NEXT_UPDATE_TIME]     = next_update_time
    sensors[NEXT_UPDATE]          = next_update_time

    sensors[LAST_UPDATE_DATETIME] = datetime_now()
    sensors[LAST_UPDATE_TIME]     = time_now()
    sensors[LAST_UPDATE]          = time_now()

    if Device_or_FromZone is None:
        return sensors

    Device_or_FromZone.interval_secs    = interval_secs
    Device_or_FromZone.interval_str     = secs_to_time_str(interval_secs)
    Device_or_FromZone.next_update_secs = next_update_secs
    Device_or_FromZone.next_update_time = next_update_time
    Device_or_FromZone.last_update_secs = Gb.this_update_secs
    Device_or_FromZone.last_update_time = time_to_12hrtime(Gb.this_update_time)

    Device_or_FromZone.sensors.update(sensors)

    return sensors

#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#
#   TRACK FROM ZONE FUNCTIONS
#
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def determine_TrackFrom_zone(Device):
    '''
    The FromZone tracking sensors have the results i.e., zone distance, interval, next update time
    and direction calculations, etc. The TrackFmZone sensor data that is closest to the Device's
    location are copied to the Device sensors if the Device is < 100km from home and 8km of the TrackFmZone
    or > 100km from Home
    '''
    if Device.only_track_from_home: return

    # track_fm_zone_radius is the distance from the trackFm zone before it's results are displayed
    # track_fm_zone_home_radius is the max home zone distance before the closest trackFm results are dsplayed

    Device.FromZone_TrackFrom = Device.TrackFromBaseZone
    for from_zone, FromZone in Device.FromZones_by_zone.items():
        if FromZone.next_update_secs <= Device.FromZone_TrackFrom.next_update_secs:
            # If within tfz tracking dist, display this tfz results
            # Then see if another trackFmZone is closer to the device
            if (FromZone.zone_dist <= Gb.tfz_tracking_max_distance
                    and FromZone.zone_dist < Device.FromZone_TrackFrom.zone_dist):
                Device.FromZone_TrackFrom = FromZone

    # If this is the last zone exited and going away from it, use Home instead
    if Device.FromZone_TrackFrom is not Device.TrackFromBaseZone:
        if Device.FromZone_TrackFrom.dir_of_travel == AWAY_FROM:
            Device.FromZone_TrackFrom = Device.TrackFromBaseZone

#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#
#   UPDATE DEVICE LOCATION & INFORMATION ATTRIBUTE FUNCTIONS
#
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def _get_distance_data(Device, FromZone):
    """
    Determine the location of the device.
        Returns:
            - zone (current zone from lat & long)
                set to HOME if distance < home zone radius
            - dist_from_zone_km (mi or km)
            - dist_traveled (since last poll)
            - dir_of_travel (towards, away_from, stationary, in_zone,
                left_zone, near_home)
    """

    Device.display_info_msg(f"GetDistancesFrom-{FromZone.from_zone_display_as}")

    if Device.no_location_data:
        event_msg = "No location data available, will retry"
        post_event(Device.devicename, event_msg)
        return (ERROR, {})

    calc_dist_from_zone_km = dist_from_zone_km   = FromZone.distance_km
    dist_from_zone_m       = dist_from_zone_km * 1000
    waze_dist_moved_km     = dist_moved_km       = Device.loc_data_dist_moved_km
    waze_dist_from_zone_km = calc_dist_from_zone_km
    waze_time_from_zone    = 0
    from_zone              = FromZone.from_zone

    # Device is in the from_zone so nothing to do
    if Device.loc_data_zone == from_zone:
        Device.statzone_reset_timer
        Gb.Waze.waze_status = WAZE_PAUSED
        Gb.Waze.waze_close_to_zone_pause_flag = True
        distance_data = [VALID_DATA,
                        0,                          # dist_from_zone_km,
                        dist_from_zone_m,           # dist_from_zone_m,
                        0,                          # waze_dist_from_zone_km,
                        calc_dist_from_zone_km,     # calc_dist_from_zone_km,
                        0,                          # waze_time_from_zone,
                        dist_moved_km,              # dist moved
                        INZONE]                     # direction

        return distance_data

    #--------------------------------------------------------------------------------
    Gb.Waze.waze_status = WAZE_USED if Gb.Waze.distance_method_waze_flag else WAZE_NOT_USED
    waze_source_msg = ''
    if Gb.Waze.is_status_USED:
        # See if this location hasn't changed or is in the history db
        if Gb.WazeHist.is_historydb_USED and calc_dist_from_zone_km < Gb.WazeHist.max_distance:
            waze_status, waze_time_from_zone, waze_dist_from_zone_km, dist_moved_km, \
                hist_db_location_id, waze_source_msg = \
                Gb.Waze.get_history_time_distance(Device, FromZone, check_hist_db=True)
        else:
            hist_db_location_id = 0

        # Not in history db or history db is not used
        if hist_db_location_id == 0:
            waze_dist_from_zone_km = calc_dist_from_zone_km
            waze_time_from_zone    = 0

        if hist_db_location_id > 0:
            pass

        # Pause waze and set close to zone pause flag if nearing a track from zone
        elif (calc_dist_from_zone_km < 1
                and Device.loc_data_zone == from_zone
                and FromZone.is_going_towards):
            Gb.Waze.waze_status = WAZE_PAUSED
            Gb.Waze.waze_close_to_zone_pause_flag = True
            dist_from_zone_km = calc_dist_from_zone_km

        #Determine if Waze should be used based on calculated distance
        elif (calc_dist_from_zone_km > Gb.Waze.waze_max_distance
                or calc_dist_from_zone_km < Gb.Waze.waze_min_distance):
            Gb.Waze.waze_status = WAZE_OUT_OF_RANGE
            dist_from_zone_km = calc_dist_from_zone_km

    dist_from_zone_m = dist_from_zone_km * 1000

    # Get Waze travel_time & distance
    if Gb.Waze.is_status_USED:
        if hist_db_location_id == 0:
            waze_status, waze_time_from_zone, waze_dist_from_zone_km, waze_dist_moved_km \
                    = Gb.Waze.get_route_time_distance(Device, FromZone, check_hist_db=False)

            Gb.Waze.waze_status = waze_status

        # Don't reset data if poor gps, use the best we have
        dist_from_zone_m = dist_from_zone_km * 1000
        Device.display_info_msg( f"Finalizing-{FromZone.info_status_msg}")
        if Device.loc_data_zone == from_zone:
            dist_from_zone_km = 0
            dist_moved_km     = 0

        elif Gb.Waze.is_status_USED:
            dist_from_zone_km = waze_dist_from_zone_km
            dist_from_zone_m  = waze_dist_from_zone_km * 1000
            dist_moved_km     = waze_dist_moved_km
    else:
        waze_dist_from_zone_km = 0

    if waze_source_msg:
        event_msg = f"Waze Route Info > {waze_source_msg}"
        post_event(Device.devicename, event_msg)

    #--------------------------------------------------------------------------------
    dir_of_travel = '___'
    time_change_secs = 0 if waze_time_from_zone == 0 else \
        int(waze_time_from_zone * 60) - int(FromZone.waze_time * 60)
    dist_moved_m = int(dist_from_zone_m - FromZone.zone_dist_m)

    if FromZone.dir_of_travel in [INZONE, STATIONARY_FNAME]:
        FromZone.dir_of_travel = STATIONARY_FNAME

    if Device.is_in_statzone:
        dir_of_travel = STATIONARY_FNAME

    elif Device.is_inzone:
        dir_of_travel = INZONE

    elif Device.sensors[ZONE] == NOT_SET or FromZone.dir_of_travel == NOT_SET:
        dir_of_travel = Device.sensors[DIR_OF_TRAVEL]

    # Use last direction if did not move
    elif time_change_secs == 0 or dist_moved_m == 0:
        if FromZone.dir_of_travel == UNKNOWN:
            dir_of_travel = STATIONARY_FNAME
        else:
            dir_of_travel = FromZone.dir_of_travel

    # Towards if the last zone distance > than this zone distance
    elif time_change_secs < 0 or dist_moved_m < 1:
        dir_of_travel = TOWARDS

    elif time_change_secs > 1 or dist_moved_m > 1:
        dir_of_travel = AWAY_FROM

    else:
        #didn't move far enough to tell current direction
        dir_of_travel = FromZone.dir_of_travel

    if dir_of_travel.startswith('@'): dir_of_travel = INZONE

    if Device.loc_data_zone == NOT_HOME:
        if Gb.is_statzone_used is False:
            pass

        elif Device.statzone_timer == 0:
            Device.statzone_reset_timer

        # If moved more than stationary zone limit (~.06km(200ft)),
        # reset StatZone still timer
        # Use calc distance rather than waze for better accuracy
        elif (calc_dist_from_zone_km > Gb.statzone_min_dist_from_zone_km
                and Device.loc_data_dist_moved_km > Gb.statzone_dist_move_limit_km
                and Device.is_tracked):
            Device.statzone_reset_timer

            event_msg =(f"StatZone Timer Reset > "
                        f"NewTime-{secs_to_time(Device.statzone_timer)}, "
                        f"Moved-{format_dist_km(Device.loc_data_dist_moved_km)}")
                        # f"(>{format_dist_km(Gb.statzone_dist_move_limit_km)})")
            post_event(Device.devicename, event_msg)

    dist_from_zone_km      = round_to_zero(dist_from_zone_km)
    dist_moved_km          = round_to_zero(dist_moved_km)
    waze_dist_from_zone_km = round_to_zero(waze_dist_from_zone_km)
    waze_dist_moved_km     = round_to_zero(waze_dist_moved_km)

    distance_data = [VALID_DATA,
                    dist_from_zone_km,
                    dist_from_zone_m,
                    waze_dist_from_zone_km,
                    calc_dist_from_zone_km,
                    waze_time_from_zone,
                    dist_moved_km,
                    dir_of_travel]

    return  distance_data

#--------------------------------------------------------------------------------
def _get_interval_for_error_retry_cnt(Device, counter=OLD_LOC_POOR_GPS_CNT, pause_control_flag=False):
    '''
    Get the interval time based on the retry_cnt.
    retry_cnt   =   poor_location_gps count (default)
                =   iosapp_request_loc_sent_retry_cnt
                =   retry pyicloud authorization count
    pause_control_flag = True if device will be paused (interval is negative)
                = False if just getting interfal and device will not be paused

    Returns     interval in minutes
                (interval is negative if device should be paused)

    Interval range table - key = retry_cnt, value = time in minutes
    - poor_location_gps cnt, icloud_authentication cnt (default):
        interval_range_1 = {0:.25, 4:1, 8:5,  12:30, 16:60, 20:120, 24:240}
    - request iosapp location retry cnt:
        interval_range_2 = {0:.5,  4:2, 8:30, 12:60, 16:120}

    '''
    if counter == OLD_LOC_POOR_GPS_CNT:
        retry_cnt = Device.old_loc_poor_gps_cnt
        range_tbl = RETRY_INTERVAL_RANGE_1

    elif counter == AUTH_ERROR_CNT:
        retry_cnt = Gb.icloud_acct_auth_error_cnt
        range_tbl = RETRY_INTERVAL_RANGE_1

    elif counter == IOSAPP_REQUEST_LOC_CNT:
        retry_cnt = Device.iosapp_request_loc_retry_cnt
        range_tbl = RETRY_INTERVAL_RANGE_2
    else:
        return 60

    interval_secs = .25
    for k, v in range_tbl.items():
        if k <= retry_cnt:
            interval_secs = v

    if pause_control_flag is False: interval_secs = abs(interval_secs)
    interval_secs = interval_secs * 60

    return interval_secs

#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#
#   If a NearDevice is available, Make sure that it is not a circular reference by checking
#   the NearDevice's NearDevice. It should not point back to this Device. If so, don't use it.
#   If OK, use the results of another device that is nearby. It must be
#
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def used_near_device_results(Device, FromZone):
    if (Device.NearDevice is None
            or Device.NearDevice.NearDevice is Device
            or FromZone.from_zone not in Device.NearDevice.FromZones_by_zone):
        Device.near_device_used = ''
        return False

    neardevice_fname      = Device.NearDevice.fname_devtype
    neardevice_fname_chk  = f"{NEAR_DEVICE_USEABLE_SYM}{neardevice_fname}"
    Device.dist_apart_msg = Device.dist_apart_msg.replace(NEAR_DEVICE_USEABLE_SYM, '')
    Device.dist_apart_msg = Device.dist_apart_msg.replace(neardevice_fname, neardevice_fname_chk)
    Device.near_device_used = f"{Device.NearDevice.fname_devtype} ({format_dist_m(Device.near_device_distance)})"
    event_msg =(f"Using Nearby Device Results > {Device.NearDevice.fname}, "
                f"Distance-{format_dist_m(Device.near_device_distance)}")
    post_event(Device.devicename, event_msg)

    copy_near_device_results(Device, FromZone)

    Device.display_info_msg(Device.format_info_msg, new_base_msg=True)
    post_results_message_to_event_log(Device, FromZone,)
    post_zone_time_dist_event_msg(Device, FromZone)

    return True

#--------------------------------------------------------------------------------
def copy_near_device_results(Device, FromZone):
    '''
    The Device is near the NearDevice for the FromZone zone results. Copy the NearDevice
    variables to Device since everything is the same.
    '''
    NearDevice       = Device.NearDevice
    from_zone        = FromZone.from_zone
    NearFromZone = Device.NearDevice.FromZones_by_zone[from_zone]

    Device.loc_data_zone           = NearDevice.loc_data_zone
    Device.zone_change_secs        = NearDevice.zone_change_secs

    FromZone.zone_dist         = NearFromZone.zone_dist
    FromZone.waze_dist         = NearFromZone.waze_dist
    FromZone.waze_time         = NearFromZone.waze_time
    FromZone.calc_dist         = NearFromZone.calc_dist
    FromZone.interval_method   = NearFromZone.interval_method
    FromZone.last_update_secs  = NearFromZone.last_update_secs
    FromZone.last_update_time  = NearFromZone.last_update_time
    FromZone.last_travel_time  = NearFromZone.last_travel_time
    FromZone.last_distance_km  = NearFromZone.last_distance_km
    FromZone.last_distance_str = NearFromZone.last_distance_str
    FromZone.dir_of_travel     = NearFromZone.dir_of_travel
    if NearFromZone.zone_dist < 100:
        FromZone.dir_of_travel_history += NearFromZone.dir_of_travel[0:1]
        monitor_msg = (f"DirHist-{FromZone.format_dir_of_travel_history}")
        post_monitor_msg(Device.devicename, monitor_msg)

    FromZone.sensors.update(NearFromZone.sensors)

    if Device.is_in_statzone:
        interval_secs = Device.statzone_inzone_interval_secs
    elif Device.is_inzone:
        interval_secs = Device.inzone_interval_secs
    else:
        interval_secs = NearFromZone.interval_secs

    _update_next_update_fields_and_sensors(FromZone, interval_secs)

    Device.StatZone                = NearDevice.StatZone
    Device.statzone_timer          = NearDevice.statzone_timer
    Device.statzone_dist_moved_km  = NearDevice.statzone_dist_moved_km
    Device.iosapp_request_loc_sent_secs = Gb.this_update_secs

    log_rawdata(f"{Device.devicename} - {from_zone}", FromZone.sensors)

    return FromZone.sensors

#--------------------------------------------------------------------------------
def update_near_device_info(Device):
    '''
    Cycle through the devices and see if this device is in the same location as
    another device updated earlier in this 5-sec polling loop.

    Return: The closest device

    {devicename: [dist_m, gps_accuracy_factor, display_text]}
    '''
    if (len(Gb.Devices) == 1
            or len(Device.dist_to_other_devices) == 0
            or Gb.distance_between_device_flag is False):
        return

    closest_device_distance     = HIGH_INTEGER
    Device.dist_apart_msg       = ''
    Device.NearDevice           = None
    Device.near_device_distance = 0
    Device.near_device_checked_secs = time_now_secs()

    for devicename, dist_apart_data in Device.dist_to_other_devices.items():
        _Device = Gb.Devices_by_devicename[devicename]

        dist_apart_m, min_gps_accuracy, loc_data_secs, display_text = dist_apart_data

        # if one device is a watch and the devices are paired and the watch is close to the
        # device, the watch may use the devices location info. Don't use the watch as nearby
        # â¦¾Ã—âŒ˜â›’â™ºâš¯âš ï¸Žâš®âˆž
        if ((Device.device_type == WATCH or _Device.device_type == WATCH)
                and Device.paired_with_id == _Device.paired_with_id
                and dist_apart_m < NEAR_DEVICE_DISTANCE):
            useable_symbol = 'Ã—'
        elif dist_apart_m > NEAR_DEVICE_DISTANCE:
            useable_symbol = '>'
        elif min_gps_accuracy > NEAR_DEVICE_DISTANCE:
            useable_symbol = 'Â±'
        elif instr(display_text, '+') or instr(display_text, 'Â±'):      # old or gps accuracy issue
            useable_symbol = 'Ã—'
        elif _Device.NearDevice is Device:
            useable_symbol = 'âŒ˜'
        elif _check_near_device_circular_loop(_Device, Device) is False:
            useable_symbol = 'âŒ˜'
        elif Device.loc_data_zone != _Device.loc_data_zone:
            useable_symbol = 'Ã—'
        else:
            useable_symbol = NEAR_DEVICE_USEABLE_SYM

        age_secs = secs_since(loc_data_secs)
        time_msg = f"/{secs_to_time_str(age_secs).replace(' ', '+ ')} ago" if age_secs > 120 else ''
        gps_msg  = f"Â±{min_gps_accuracy}" if min_gps_accuracy > Gb.gps_accuracy_threshold else ''

        Device.dist_apart_msg += (  f"{useable_symbol}{_Device.fname_devtype}-"
                                    f"{format_dist_m(dist_apart_m)}{gps_msg}{time_msg}, ")

        # The nearby devices can not point to each other and other criteria
        if (Device.is_tracked
                and _Device.is_tracked
                and useable_symbol == NEAR_DEVICE_USEABLE_SYM
                and _Device.FromZone_Home.interval_secs > 0
                and _Device.old_loc_poor_gps_cnt == 0
                and _Device.is_online):

            if dist_apart_m < closest_device_distance:
                closest_device_distance = dist_apart_m
                Device.NearDevice = _Device
                Device.near_device_distance = dist_apart_m

    monitor_msg = f"Nearby Devices > ({LT}{NEAR_DEVICE_DISTANCE}m), {Device.dist_apart_msg}"
    post_monitor_msg(Device.devicename, monitor_msg)

    return

#--------------------------------------------------------------------------------
def _check_near_device_circular_loop(_Device, Device):
    '''
    Make sure the eligible nearbe device is not used by another device(s) that ends
    up referencing the Device being updated, creating a circular loop back to itself
    '''
    if _Device.NearDevice is None:
        return True

    next_Device_to_check = _Device.NearDevice
    checked_Devices = []
    near_devices_msg = f"{_Device.devicename}{RARROW}{next_Device_to_check.devicename}"
    log_debug_base_msg = f"Check Nearby Device-{_Device.devicename}"
    reason_msg = ''

    check_start_time_secs = time_now_secs()
    cnt = 0
    can_use_device = False

    while can_use_device is False:     #next_Device_to_check is not Device:
        # Make sure the loop will not hang
        cnt += 1
        if cnt > len(Gb.Devices):
            reason_msg = f"DeviceCnt-{cnt} > {len(Gb.Devices)}"
            break

        # Make sure the loop does not hang by only running 5-secs
        if secs_since(check_start_time_secs) > 5:
            reason_msg = 'Running more than 10-secs'
            break

        # If the next_NearDevice will loop back to a device that has already been checked,
        # do not use the _Device.NearDevice
        if next_Device_to_check.NearDevice in checked_Devices:
            near_devices_msg += f"{RARROW}{_Device.devicename}"
            reason_msg = 'Looped back to itself'
            break

        # If the next_NearDevice circles back to the _Device originally being checked,
        # do not use the _Device.NearDevice
        if next_Device_to_check.NearDevice is _Device:
            near_devices_msg += f"{RARROW}{_Device.devicename}"
            reason_msg = 'Looped back to start'
            break

        # If the NearDevice cycle ends, it's OK to use the current _Device
        if (next_Device_to_check is None
                or next_Device_to_check.NearDevice is None):
            near_devices_msg += f"{RARROW}None"
            can_use_device = True
            break

        near_devices_msg += f"{RARROW}{next_Device_to_check.NearDevice.devicename}"
        next_Device_to_check = next_Device_to_check.NearDevice
        checked_Devices.append(next_Device_to_check.NearDevice)
        log_msg = ( f"{log_debug_base_msg}, Checking, "
                    f"Cnt-{cnt}, "
                    f"Timer-{secs_since(check_start_time_secs)} secs, "
                    f"{near_devices_msg}")
        log_debug_msg(Device.devicename, log_msg)

    # The loop was broken out of
    can_cannot_msg = 'CanUse' if can_use_device else f"CanNotUse, {reason_msg}"
    log_msg = ( f"{log_debug_base_msg}, "
                f"{can_cannot_msg}, "
                f"Cnt-{cnt}, "
                f"Timer-{secs_since(check_start_time_secs)} secs, "
                f"{near_devices_msg}")
    log_debug_msg(Device.devicename, log_msg)
    return can_use_device


from typing import List
from datetime import date
from fastapi import APIRouter, Depends, Query, HTTPException
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session

from schemas.timelog import (
    TaskTypeItem,
    Template as TemplateSchema,
    TemplateNew as TemplateNewSchema,
    TemplateUpdate as TemplateUpdateSchema,
    Task as TaskSchema,
    TaskNew as TaskNewSchema,
    TaskUpdate as TaskUpdateSchema,
)
from schemas.validation import ValidatedObject
from services.timelog import TaskTypeService, TemplateService, TaskService
from services.projects import ProjectService
from services.config import ConfigService
from db.db_connection import get_db
from auth.auth_bearer import BearerToken
from dependencies import get_current_user, AppUser
from helpers.time import time_string_to_int

router = APIRouter(
    prefix="/timelog",
    tags=["timelog"],
    responses={403: {"description": "Forbidden"}, 404: {"description": "Not found"}},
    dependencies=[Depends(BearerToken())],
)

@router.get("/task_types/", response_model=List[TaskTypeItem])
async def get_task_types(
    current_user=Depends(get_current_user), db: Session = Depends(get_db), skip: int = 0, limit: int = 100
):
    items = TaskTypeService(db).get_items()
    return items

@router.get("/templates", response_model=List[TemplateSchema])
async def get_user_templates(user_id: int, current_user=Depends(get_current_user), db: Session = Depends(get_db)):
    if user_id != current_user.id:
        raise HTTPException(status_code=403, detail="You are not authorized to see templates for this user")
    templates = TemplateService(db).get_user_templates(user_id)
    return templates

@router.post("/templates", response_model=TemplateSchema, status_code=201)
async def add_template(
    template: TemplateNewSchema, current_user=Depends(get_current_user), db: Session = Depends(get_db)
):
    """
    Create a user or global template. Required fields are: `name` and `is_global`.
    In case the template is not global, the `user_id` field is also mandatory.
    \nBoth `start` and `end` times are in 24h time notation (HH:mm).
    """
    validated_template = validate_template(template, db, current_user)
    if not validated_template.is_valid:
        raise HTTPException(status_code=422, detail=validated_template.message)
    result = TemplateService(db).create_template(template)
    return result

@router.put("/templates/{template_id}", response_model=TemplateSchema)
async def update_template(
    template_id: int,
    template: TemplateUpdateSchema,
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    """
    Update a template. Required fields are: `name` and `is_global`.
    In case the template is not global, the `user_id` field is also mandatory.
    \nBoth `start` and `end` times are in 24h time notation (HH:mm).
    """
    existing_template = TemplateService(db).get_template(template_id)
    if not existing_template:
        raise HTTPException(status_code=404, detail=f"Template with id {template_id} not found")
    validated_template = validate_template(template, db, current_user)
    if not validated_template.is_valid:
        raise HTTPException(status_code=422, detail=validated_template.message)
    result = TemplateService(db).update_template(existing_template, template)
    return result

@router.delete("/templates/{template_id}", status_code=204)
async def delete_template(template_id: int, current_user=Depends(get_current_user), db: Session = Depends(get_db)):
    template = TemplateService(db).get_template(template_id)
    if not template:
        raise HTTPException(status_code=404, detail=f"Template with id {template.id} not found.")
    if template.is_global and "manager" not in current_user.roles:
        raise HTTPException(status_code=403, detail="You are not authorized to delete global templates")
    else:
        if template.user_id and template.user_id != current_user.id:
            raise HTTPException(status_code=403, detail="You are not authorized to delete templates for this user")
    TemplateService(db).delete_template(template_id)
    return

@router.get("/tasks", response_model=List[TaskSchema])
async def get_user_tasks(
    user_id: int,
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
    offset: int = Query(0, ge=0),
    limit: int = Query(25, ge=0, le=500),
    start: date = date.today(),
    end: date = date.today(),
):
    if current_user.id != user_id and "manager" not in current_user.roles:
        raise HTTPException(status_code=403, detail="You are not authorized to view tasks for this user")
    tasks = TaskService(db).get_user_tasks(user_id, offset, limit, start, end)
    return tasks

@router.post("/tasks", response_model=TaskSchema, status_code=201)
async def add_task(task: TaskNewSchema, current_user=Depends(get_current_user), db: Session = Depends(get_db)):
    """
    Create a task with the following data:

    - **user id**: the user id
    - **project_id**: the project the task is associated with
    - **story**: the task story
    - **description**: the task description
    - **task type**: the task type
    - **date**: the task date (YYYY-MM-DD)
    - **start time**: the task start time in 24h time notation (HH:mm)
    - **end time**: the task end time in 24h time notation (HH:mm)

    \f
    :param item: User input.
    """
    if current_user.id != task.user_id:
        raise HTTPException(status_code=403, detail="You are not authorized to create tasks for this user.")
    validated_task = validate_task(task, db)
    if not validated_task.is_valid:
        raise HTTPException(status_code=422, detail=validated_task.message)
    result = TaskService(db).create_task(task)
    return result

@router.put("/tasks/{task_id}", response_model=TaskSchema)
async def update_task(
    task_id: int, task: TaskUpdateSchema, current_user=Depends(get_current_user), db: Session = Depends(get_db)
):
    """
    Update an task with any of the following data:

    - **user id**: the user id
    - **project_id**: the project the task is associated with
    - **story**: the task story
    - **description**: the task description
    - **task type**: the task type
    - **date**: the task date (YYYY-MM-DD)
    - **start time**: the task start time in 24h time notation (HH:mm)
    - **end time**: the task end time in 24h time notation (HH:mm)

    \f
    :param item: User input.
    """
    existing_task = TaskService(db).get_task(task_id)
    if not existing_task:
        raise HTTPException(status_code=404, detail=f"Task with id {task_id} not found")
    if current_user.id != existing_task.user_id:
        raise HTTPException(status_code=403, detail="You are not authorized to update tasks for this user.")
    if task.start_time:
        task.init = time_string_to_int(task.start_time)
    if task.end_time:
        task.end = time_string_to_int(task.end_time)
    existing_data = jsonable_encoder(existing_task)
    update_data = task.dict(exclude_unset=True)
    for field in existing_data:
        if field in update_data:
            setattr(existing_task, field, update_data[field])
    validated_task = validate_task(existing_task, db)
    if not validated_task.is_valid:
        raise HTTPException(status_code=422, detail=validated_task.message)
    result = TaskService(db).update_task(existing_task, task)
    return result

@router.delete("/tasks/{task_id}", status_code=204)
async def delete_task(task_id: int, current_user=Depends(get_current_user), db: Session = Depends(get_db)):
    task = TaskService(db).get_task(task_id)
    if not task:
        raise HTTPException(status_code=404, detail=f"Task with id {task_id} not found")
    if current_user.id != task.user_id:
        raise HTTPException(status_code=403, detail="You are not authorized to delete tasks for this user")
    TaskService(db).delete_task(task_id)
    return

def validate_task(task_to_validate: TaskSchema, db: Session):
    validated = ValidatedObject(is_valid=False, message="")
    user_can_create_tasks = ConfigService(db).can_user_edit_task(task_to_validate.date)
    if not user_can_create_tasks:
        validated.message += "You cannot create or edit a task for this date - it is outside the allowed range."
        return validated
    if task_to_validate.task_type == "":
        task_to_validate.task_type = None
    if task_to_validate.task_type:
        task_type_valid = TaskTypeService(db).slug_is_valid(task_to_validate.task_type)
        if not task_type_valid:
            validated.message += f"Task type {task_to_validate.task_type} does not exist."
            return validated
    project_active = ProjectService(db).is_project_active(task_to_validate.project_id)
    if not project_active:
        validated.message += "You cannot add a task for an inactive project."
        return validated
    overlapping_tasks = TaskService(db).check_task_for_overlap(task_to_validate)
    if not overlapping_tasks.is_valid:
        validated.message += overlapping_tasks.message
        return validated
    validated.is_valid = True
    return validated

def validate_template(template: TemplateSchema, db: Session, current_user: AppUser):
    validated = ValidatedObject(is_valid=False, message="")
    if template.is_global and "manager" not in current_user.roles:
        raise HTTPException(status_code=403, detail="You are not authorized to create or update global templates")
    else:
        if template.user_id and template.user_id != current_user.id:
            raise HTTPException(
                status_code=403, detail="You are not authorized to create or update templates for this user"
            )
    if template.task_type:
        task_type_valid = TaskTypeService(db).slug_is_valid(template.task_type)
        if not task_type_valid:
            validated.message += f"Task type {template.task_type} does not exist."
            return validated
    if template.project_id:
        project_active = ProjectService(db).is_project_active(template.project_id)
        if not project_active:
            validated.message += "You cannot add a template for an inactive project."
            return validated
    validated.is_valid = True
    return validated


import json, re, select, random
from struct import pack, unpack

import asyncio, aiohttp

from .tars import tarscore

class Huya:
    heartbeat = b"\x00\x03\x1d\x00\x00\x69\x00\x00\x00\x69\x10\x03\x2c\x3c\x4c\x56\x08\x6f\x6e\x6c\x69\x6e\x65\x75\x69\x66\x0f\x4f\x6e\x55\x73\x65\x72\x48\x65\x61\x72\x74\x42\x65\x61\x74\x7d\x00\x00\x3c\x08\x00\x01\x06\x04\x74\x52\x65\x71\x1d\x00\x00\x2f\x0a\x0a\x0c\x16\x00\x26\x00\x36\x07\x61\x64\x72\x5f\x77\x61\x70\x46\x00\x0b\x12\x03\xae\xf0\x0f\x22\x03\xae\xf0\x0f\x3c\x42\x6d\x52\x02\x60\x5c\x60\x01\x7c\x82\x00\x0b\xb0\x1f\x9c\xac\x0b\x8c\x98\x0c\xa8\x0c"

    async def get_ws_info(url):
        reg_datas = []
        url = "https://m.huya.com/" + url.split("/")[-1]
        headers = {
            "user-agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Mobile Safari/537.36"
        }
        async with aiohttp.ClientSession() as session:
            async with session.get(url, headers=headers) as resp:
                room_page = await resp.text()
                # print(room_page)
                m = re.search(r"window.HNF_GLOBAL_INIT *= *(\{.+?\})\s*</script>", room_page, re.MULTILINE)
                j = json.loads(m.group(1))
                ayyuid = j["roomInfo"]["tProfileInfo"]["lUid"]
                # tid = j["roomInfo"]["tLiveInfo"]["tLiveStreamInfo"]["vStreamInfo"]["value"][0]["lChannelId"]
                # sid = j["roomInfo"]["tLiveInfo"]["tLiveStreamInfo"]["vStreamInfo"]["value"][0]["lSubChannelId"]

                # print(ayyuid)
                # print(tid)
                # print(sid)

        # a = tarscore.string

        l = tarscore.vctclass(tarscore.string)()
        l.append(f"live:{ayyuid}")
        l.append(f"chat:{ayyuid}")
        oos = tarscore.TarsOutputStream()
        oos.write(tarscore.vctclass(tarscore.string), 0, l)
        oos.write(tarscore.string, 1, "")

        # oos.write(tarscore.int64, 0, int(ayyuid))
        # oos.write(tarscore.boolean, 1, True)  # Anonymous
        # oos.write(tarscore.string, 2, "")  # sGuid
        # oos.write(tarscore.string, 3, "")
        # oos.write(tarscore.int64, 4, int(tid))
        # oos.write(tarscore.int64, 5, int(sid))
        # oos.write(tarscore.int64, 6, 0)
        # oos.write(tarscore.int64, 7, 0)

        wscmd = tarscore.TarsOutputStream()
        wscmd.write(tarscore.int32, 0, 16)
        # wscmd.write(tarscore.int32, 0, 1)
        wscmd.write(tarscore.bytes, 1, oos.getBuffer())

        reg_datas.append(wscmd.getBuffer())
        return "wss://cdnws.api.huya.com/", reg_datas

    def decode_msg(data):
        class user(tarscore.struct):
            def readFrom(ios):
                return ios.read(tarscore.string, 2, False).decode("utf8")

        class dcolor(tarscore.struct):
            def readFrom(ios):
                return ios.read(tarscore.int32, 0, False)

        name = ""
        content = ""
        msgs = []
        ios = tarscore.TarsInputStream(data)

        if ios.read(tarscore.int32, 0, False) == 7:
            ios = tarscore.TarsInputStream(ios.read(tarscore.bytes, 1, False))
            if ios.read(tarscore.int64, 1, False) == 1400:
                ios = tarscore.TarsInputStream(ios.read(tarscore.bytes, 2, False))
                name = ios.read(user, 0, False)  # username
                content = ios.read(tarscore.string, 3, False).decode("utf8")  # content
                color = ios.read(dcolor, 6, False)  # danmaku color
                if color == -1:
                    color = 16777215

        if name != "":
            msg = {"name": name, "color": f"{color:06x}", "content": content, "msg_type": "danmaku"}
        else:
            msg = {"name": "", "content": "", "msg_type": "other"}
        msgs.append(msg)
        return msgs


import numpy as np
import matplotlib.pyplot as plt
from apm import *

######################################################
# Configuration
######################################################
# number of terms
ny = 3 # output coefficients
nu = 3 # input coefficients
# number of inputs
ni = 2
# number of outputs
no = 2
# load data and parse into columns
data = np.loadtxt('data_no_headers.csv',delimiter=',')
######################################################

# generate time-series model
ypred = sysid(data,ni,nu,ny)

# plot results
plt.figure(1)
plt.subplot(2,1,1)
plt.plot(data[:,0],ypred[:,0],'r-',LineWidth=2)
plt.plot(data[:,0],data[:,3],'b--',LineWidth=2)
plt.plot(data[:,0],ypred[:,1],'k-',LineWidth=2)
plt.plot(data[:,0],data[:,4],'g:',LineWidth=2)
plt.legend(['Predicted 1','Measured 1',\
            'Predicted 2','Measured 2'],loc='best')
plt.ylabel('Temp (degC)')

plt.subplot(2,1,2)
plt.plot(data[:,0],data[:,1],'r-',LineWidth=2)
plt.plot(data[:,0],data[:,2],'b--',LineWidth=2)
plt.legend(['Heater 1','Heater 2'],loc='best')
plt.ylabel('Heater')
plt.show()


from datetime import datetime
import scrapy
import lxml
from lxml.html.clean import Cleaner
import re

SOURCE = 'El País'
LANGUAGE = 'es'
cleaner = Cleaner(allow_tags=['p', 'br', 'b', 'a', 'strong', 'i', 'em'])
class LanacionSpider(scrapy.Spider):
    name = 'elpais'
    allowed_domains = ['elpais.com.uy']
    start_urls = ['http://elpais.com.uy']

    def start_requests(self):
        url = getattr(self, 'article_url', None)
        if url is not None:
            yield scrapy.Request(url, callback=self.parse_article, cb_kwargs=dict(url=url))

    def parse(self, response):
        urls = []
        for article in response.css('article'):
            link = article.css('a')
            url = link.attrib['href']
            if not url:
                continue
            if not url.startswith('http'):
                url = 'https://www.elpais.com.uy' + url
            urls.append(url)

            maybe_img = article.css('.image-container img')
            obj = {
                'title': article.css('.title a::text').get(''),
                'volanta': article.css('.supratitle a::text').get('').strip(),
                'section': article.css('.category a::text').get('').strip(),
                'url': url,
                'image': maybe_img.attrib['src'] if maybe_img else None,
                'source': SOURCE,
                'source_language': LANGUAGE,
            }

            yield obj
            request = scrapy.Request(url, callback=self.parse_article, cb_kwargs=dict(url=url))
            yield request
        yield {'homepage': urls, 'source': SOURCE}

    def parse_article(self, response, url):
        html = ''.join(response.xpath('//div[@class="article-content"]//p').extract())
        if not html:
            return
        content = lxml.html.tostring(cleaner.clean_html(lxml.html.fromstring(html))).decode('utf-8')

        date = response.css('#article-published-at-date').attrib['data-published-at-date']
        date_fragments = re.match(r'^.+, ([0-9]{1,2}) ([a-z]+) ([0-9]{4}) ([0-9]{1,2}):([0-9]{2})$', date)
        months = {
            'enero': 1,
            'febrero': 2,
            'marzo': 3,
            'abril': 4,
            'mayo': 5,
            'junio': 6,
            'julio': 7,
            'agosto': 8,
            'septiembre': 9,
            'octubre': 10,
            'noviembre': 11,
            'diciembre': 12,
        }
        if date_fragments is not None:
            day = int(date_fragments.group(1))
            month = months[date_fragments.group(2)]
            year = int(date_fragments.group(3))
            hour = int(date_fragments.group(4))
            minute = int(date_fragments.group(5))
            date = datetime(year, month, day, hour, minute)
            obj = {
                'url': url,
                'content': content,
                'date': date.isoformat()
            }
        else:
            obj = {
                'url': url,
                'content': content,
            }

        yield obj


from ada import *

if __name__ == "__main__":
    sparrow = sparrow()
    toy_duck = toy_duck()
    bird_adapter = bird_adapter(sparrow)

    print("Sparrow...")
    sparrow.fly()
    sparrow.sing()

    print("ToyDuck...")
    toy_duck.squeak()

    print("BirdAdapter...")
    bird_adapter.squeak()


import time
import torch
import matplotlib.pyplot as plt

from sklearn.utils import shuffle
from Models.linearnet import LinearNet

class Optimization:
    """ A helper class to train, test and diagnose Cost-sensitive Logistic Regression

    Attributes:
        model: CSLR model.
        optimizer: Optimizer of the network.
        train_return: List of train returns.
        val_return: List of validation returns.
        validation: Whether there is validation data.
        batch_size: Batch-size of the network.
        n_epochs: Total number of epochs.
        n_steps: Number of epochs to evaluate the results
    """

    def __init__(self, model, optimizer, config):
        """Initialises CLSR.

        Args:
            model: CSLR model.
            optimizer: Optimizer of the network.
            config: Configuration of the network.
        """

        self.model = model
        self.optimizer = optimizer
        self.train_return = []
        self.val_return = []
        self.validation = False
        self.batch_size = config.get("batch_size",32)
        self.n_epochs = config.get("n_epochs", 1000)
        self.n_steps = config.get("n_steps", self.n_epochs)

    @staticmethod
    def batch(iterable, n):
        """Creates batches."""

        l = len(iterable)
        for ndx in range(0, l, n):
            yield iterable[ndx:min(ndx + n, l)]

    def train(self, x_train, r_train, x_val=None, r_val=None):
        """Applies simple feed-forward network to an input.

        Args:
            x_train: train features
            r_train: train returns
            x_val: validation features
            r_val: validation returns
        """

        if x_val is not None or r_val is not None:
            self.validation = True
        start_time = time.time()

        for epoch in range(self.n_epochs):
            x_shuff, r_shuff = shuffle(x_train, r_train)
            self.model.train()
            for j in self.batch(range(0, len(x_shuff)),self.batch_size):
                if len(j) < 2:
                    break
                x_batch = x_shuff[j]
                r_batch = r_shuff[j]
                self.optimizer.zero_grad()
                outputs, _, _ = self.model(x_batch)
                loss = -torch.mul(outputs, r_batch).sum()
                loss.backward()
                self.optimizer.step()

            returns_train, _, _ = self.evaluate(x_train, r_train)
            self.train_return.append(returns_train)
            if self.validation is True:
                returns_val, _, _ = self.evaluate(x_val, r_val)
                self.val_return.append(returns_val)

            if ((epoch+1) % self.n_steps == 0):
                elapsed = time.time() - start_time
                print(
                    ("Epoch %d Train Return: %.3f.")  % (epoch + 1, self.train_return[-1]),
                    ((" Validation Return: %.3f. Elapsed time: %.3fs.")
                     % (self.val_return[-1], elapsed)
                     if self.validation is True else
                     " Elapsed time: %.3fs."
                     % elapsed)
                )
                start_time = time.time()

    def evaluate(self, x_test, r_test):
        """Evaluates simple feed-forward network to an input.

        Args:
            x_test: features of the evaluated data
            r_test: returns of the evaluated data

        Returns:
            Triple of Tensors for: (Total returns, decision variables, probabilities)
        """

        with torch.no_grad():
            outputs, probs, _ = self.model(x_test)
            returns = torch.mul(outputs, r_test).sum()

            return returns, outputs, probs

    def plot_return(self):
        """Draws a plot, Trains Returns vs Test Returns"""

        plt.plot(self.train_return, label="Train Return")
        plt.plot(self.val_return, label="Test Return")
        plt.legend()
        plt.title("Returns")


from Algorithms.Approximators.probability_approximator import ProbabilityApproximator
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from Algorithms.help_functions import *

class FunctionApproximation(ProbabilityApproximator):
    def __init__(self, n_x, n_a, n_y, data, epsilon=0):
        super().__init__(n_x, n_a, n_y, data, epsilon)
        self.model = RandomForestClassifier(n_jobs=1, random_state=1337)
        self.name = 'Random forest approximator'
        self.xs = data['x']
        self.histories = data['h']
        self.n_samples = len(self.xs)
        self.n_features = n_x + n_a + n_a + 1
        self.predictions_dict = {}
        self.predictions_all_actions_dict = {}
        input_data = np.zeros((self.n_samples, self.n_features))
        output_data = np.zeros(self.n_samples)
        for i in range(self.n_samples):
            x = self.xs[i]
            h = self.histories[i]
            inp, outp = self.get_features(x, h)
            input_data[i] = inp
            output_data[i] = outp
        self.model.fit(input_data, output_data)

    def get_features(self, x, history):
        last_action, last_outcome = history[-1]
        actions, outcomes = self.history_to_actions_and_outcomes(history[:-1])
        features = self.fill_feature_vector(x, actions, outcomes, last_action)
        return features, last_outcome

    def history_to_actions_and_outcomes(self, history):
        actions = np.zeros(self.n_a)
        outcomes = np.zeros(self.n_a)
        for intervention in history:
            treatment, outcome = intervention
            actions[treatment] = 1
            outcomes[treatment] = outcome
        return actions, outcomes

    def state_to_actions_and_outcomes(self, state):
        actions = np.zeros(self.n_a)
        outcomes = np.zeros(self.n_a)
        for treatment, outcome in enumerate(state):
            if outcome > -1:
                actions[treatment] = 1
                outcomes[treatment] = outcome
        return actions, outcomes

    def fill_feature_vector(self, x, actions, outcomes, last_action=None):
        if last_action is None:
            n_features = self.n_features-1
        else:
            n_features = self.n_features
        features = np.zeros(n_features)
        features[:self.n_x] = x
        features[self.n_x:self.n_x + self.n_a] = actions
        features[self.n_x + self.n_a:self.n_x + self.n_a + self.n_a] = outcomes
        if last_action is not None:
            features[self.n_x + self.n_a + self.n_a:self.n_x + self.n_a + self.n_a + 1] = last_action
        return features

    def prepare_calculation(self, x, history, action=None):
        if action is None:
            probability_of_outcome_approximation = np.zeros((self.n_a, self.n_y))
            for action in range(self.n_a):
                if history[action] == -1:
                    old_actions, old_outcomes = self.state_to_actions_and_outcomes(history)
                    features = self.fill_feature_vector(x, old_actions, old_outcomes, action)
                    h = hash_array(features)
                    if h in self.predictions_dict:
                        prediction = self.predictions_dict[h]
                    else:
                        prediction = self.model.predict_proba(features.reshape(1, -1))[0]
                        self.predictions_dict[h] = prediction
                    probability_of_outcome_approximation[action] = prediction
        else:
            old_actions, old_outcomes = self.state_to_actions_and_outcomes(history)
            features = self.fill_feature_vector(x, old_actions, old_outcomes, action)
            h = hash_array(features)
            if h in self.predictions_all_actions_dict:
                probability_of_outcome_approximation = self.predictions_all_actions_dict[h]
            else:
                probability_of_outcome_approximation = self.model.predict_proba(features.reshape(1, -1))
                self.predictions_all_actions_dict[h] = probability_of_outcome_approximation
        return probability_of_outcome_approximation

    def calculate_probability(self, x, history, action, outcome):
        probs = self.prepare_calculation(x, history, action)
        return probs[0][outcome]

    def calculate_probabilities(self, x, history, action):
        probs = self.prepare_calculation(x, history, action)
        return probs[0]

    def calculate_probability_greedy(self, state, best_outcome, use_expected_value=True):
        x, history = state
        probability_of_outcome_approximation = self.prepare_calculation(x, history)
        return super(FunctionApproximation, self).calculate_probability_greedy(
            probability_of_outcome_approximation, best_outcome, use_expected_value)

    def calculate_probability_constraint(self, x, state, accuracy=None):
        probability_of_outcome_approximation = self.prepare_calculation(x, state)
        max_outcome = max(state)
        return super(FunctionApproximation, self).calculate_probability_greedy(
            probability_of_outcome_approximation, max_outcome, use_expected_value=True)


from sys import argv

script, filename = argv

print("we are going to erase file %r" %filename)
print("if you want to abort the operation press ctrl + c")
print ("if you donot want to abort press enter")

input("? ")
print("opening the file")

target = open(filename, 'w')

print(" truncating the file")
target.truncate()

print (" now we are going to type three lines in this file")

line1 = input("type line 1 : " )
line2 =input("type line2: ")
line3 = input("type line 3:")

print("now we are going to write these typed line into this file")

target.write(line1)
target.write("\n")
target.write(line2)
target.write("\n")
target.write(line3)
target.write("\n")

print(" And closing this file now")
target.close()


"""gnsstime

extended datetime module to handle dates related to the GNSS analysis.
Supports addition of timedelta, subtraction of a datetime.

2014/12/04 Created by Satoshi Kawamoto
2014/12/09 Added 'gpscal' to the module function.
2015/01/14 Added 'from_gpsw'.
2015/01/21 Added 'gpswsec'.
2015/08/21 Added a new leapsec.
2015/11/11 Added 'mjd2date'.
2015/12/01 Added 'from_mjd' and a property 'mjd'.
2015/12/03 Fixed bug of 'gpst'. The plus-minus sign was incorrect.
2021/04/25 Fixed timezone problem.

methods:
    gpscal        : return a tuple (doy, gpsw, gpswd).
    ymd2date      : return a datetime from a string of 'yyyy/mm/dd'.
    mjd2date      : return a datetime from a modified Julian day.
    from_datetime : return a gnsstime from a datetime.
    to_datetime   : return a datetime from a gnsstime.
    from_gpsw     : return a gnsstime from a gps week.
    from_doy      : return a gnsstime from a set of yyyy, doy.
    from_mjd      : return a gnsstime from a modified julian day.

properties:
    gpsw          : GPS week from 1980/1/6.
    gpswd         : GPS weekday [0:Sunday .. 6:Saturday].
    gpswsec       : the second-of-week.
    doy           : day of the year [1-366].
    leapsec       : leap seconds for GPS time.
    gpst          : GPS time.
    mjd           : modified julian day.

Example:

>>> import gnsstime as gt
>>> gt1 = gt.gnsstime(2011, 1, 1)
>>> print("gpscal: show gps calendar (doy, gpsw, gpswd)")
>>> print("gpscal()                        -> ", gt1.gpscal())
>>> print("gpscal(2013,12,31)              -> ", gt1.gpscal(2013, 12, 31))
>>> print("gpscal(ymd='2013/12/31')        -> ", gt1.gpscal(ymd="2013/12/31"))
>>> print("gpscal(dt.datetime(2013,12,31)) -> ", gt1.gpscal(dt.datetime(2013, 12, 31)))
>>> print("gpscal(year=2013, month=12, day=31) -> ", gt.gpscal(year=2013, month=12, day=31))
gpscal()                        ->  (1, 1616, 6)
gpscal(2013,12,31)              ->  (365, 1773, 2)
gpscal(ymd='2013/12/31')        ->  (365, 1773, 2)
gpscal(dt.datetime(2013,12,31)) ->  (365, 1773, 2)
gpscal(year=2013, month=12, day=31) ->  (365, 1773, 2)
gpscal(mjd=56657)               ->  (365, 1773, 2)

>>> print("date    :", gt1.isoformat())
>>> print("doy     :", gt1.doy)
>>> print("gpsw    :", gt1.gpsw)
>>> print("gpswd   :", gt1.gpswd)
>>> print("gpst    :", gt1.gpst.isoformat())
>>> print("leapsec :", gt1.leapsec)
>>> print("mjd     :", gt1.mjd)
date    : 2011-01-01T00:00:00
doy     : 1
gpsw    : 1616
gpswd   : 6
gpst    : 2010-12-31T23:59:45
leapsec : -15
mjd     : 55562.0

>>> import datetime as dt
>>> print("gt1 + dt.timedelta(days=2) :", (gt1 + dt.timedelta(days=2)).isoformat())
>>> print("gt1 - dt.timedelta(days=2) :", (gt1 - dt.timedelta(days=2)).isoformat())
gt1 + dt.timedelta(days=2) : 2011-01-03T00:00:00
gt1 - dt.timedelta(days=2) : 2010-12-30T00:00:00
"""

import datetime as dt
from datetime import timedelta

DT_MJD0 = dt.datetime(1858,11,17, 0, 0, tzinfo=dt.timezone.utc)

class gnsstime(dt.datetime):
    """
    gnsstime(year, month, day[, hour[, minute[, second[, microsecond]]]])
    """
    # datetime at the reference of GPST, MJD
    dt_gpst0 = dt.datetime(1980, 1, 6, 0, 0, tzinfo=dt.timezone.utc)
    dt_mjd0  = dt.datetime(1858,11,17, 0, 0, tzinfo=dt.timezone.utc)
    leaps    = []

    # leap seconds for GPST
    class leapsec:
        __slots__ = ['date', 'sec']
        def __init__(self, yyyy, mm, dd, sec=0):
            self.date = dt.datetime(yyyy, mm, dd)
            self.sec  = sec
            return

    leaps.append(leapsec(2017, 1, 1, sec=-18))
    leaps.append(leapsec(2015, 7, 1, sec=-17))
    leaps.append(leapsec(2012, 7, 1, sec=-16))
    leaps.append(leapsec(2009, 1, 1, sec=-15))
    leaps.append(leapsec(2006, 1, 1, sec=-14))
    leaps.append(leapsec(1999, 1, 1, sec=-13))
    leaps.append(leapsec(1997, 7, 1, sec=-12))
    leaps.append(leapsec(1996, 1, 1, sec=-11))
    leaps.append(leapsec(1994, 7, 1, sec=-10))
    leaps.append(leapsec(1993, 7, 1, sec= -9))
    leaps.append(leapsec(1992, 7, 1, sec= -8))
    leaps.append(leapsec(1991, 1, 1, sec= -7))
    leaps.append(leapsec(1990, 1, 1, sec= -6))
    leaps.append(leapsec(1988, 1, 1, sec= -5))
    leaps.append(leapsec(1985, 7, 1, sec= -4))
    leaps.append(leapsec(1983, 7, 1, sec= -3))
    leaps.append(leapsec(1982, 7, 1, sec= -2))
    leaps.append(leapsec(1981, 7, 1, sec= -1))

    @staticmethod
    def ymd2date(ymd):
        """
        Return a datetime from ymd string e.g. "2011/1/1"
        """
        yyyy, mm, dd = ymd.strip().split('/')
        return dt.datetime(int(yyyy), int(mm), int(dd))

    @staticmethod
    def mjd2date(mjd):
        """
        Return a datetime from a modified Julian day.
        """
        return DT_MJD0 + timedelta(mjd)

    @classmethod
    def from_datetime(cls, datetime_object):
        """
        Convert a datetime to a gnsstime.
        """
        dto = datetime_object
        return cls(dto.year, dto.month, dto.day, dto.hour, dto.minute,
                   dto.second, dto.microsecond)

    @classmethod
    def from_mjd(cls, mjd):
        """
        Convert a mjd (modified julian date) to a gnsstime.
        """
        dto = cls.mjd2date(mjd)
        return cls(dto.year, dto.month, dto.day, dto.hour, dto.minute,
                   dto.second, dto.microsecond)

    @classmethod
    def from_gpsw(cls, gpsw, gpswd=None, tow=None, nrollover=0):
        """
        Convert a gps week to a gnsstime.
        """
        # 4 def calgps(gpsw, gpswd=None, tow=None, doy=None):
        #gt0 = cls(1980, 1, 6)
        seconds = 0.

        if gpswd is None:
            if tow is not None:
                gpswd = int(tow/86400.)
                seconds = tow - gpswd*86400.
            else:
                gpswd = 0.

        return cls.dt_gpst0 + dt.timedelta(days=1024*nrollover+gpsw*7+gpswd, seconds=seconds)

    @classmethod
    def from_doy(cls, year, doy):
        """
        Convert a date with year and doy to a gnsstime.
        """
        if year < 100:
            if year < 80:
                year += 2000
            else:
                year += 1900
        if doy < 1:
            print("error: doy must be positive. doy={:}".format(doy))
            return None
        return cls(year, 1, 1) + dt.timedelta(days=doy-1)

    def to_datetime(self):
        return dt.datetime(self.year, self.month, self.day, self.hour,
                           self.minute, self.second, self.microsecond)

    def _arg2date(self, *args, date, year, month, day, ymd, mjd):
        """
        Convert arguments to a gnsstime instance.
        args could be one of:
        1. year, month, day, [hour, minute, second, microsecond]
        2. datetime or gnsstime
        3. ymd string e.g. "2011/1/1"
        """
        if len(args) == 1:
            if isinstance(args[0], dt.datetime):
                tmpdate = self.from_datetime(args[0])     # datetime object
            elif isinstance(args[0], str):
                tmpdate = self.ymd2date(args[0])  # "yyyy/mm/dd"
        elif len(args) >= 3:
            # yyyy, mm, dd, [HH, MM, SS, microsec]
            tmpdate = self.from_datetime(dt.datetime(*args))
        else:
            # do not modify date
            tmpdate = self.from_datetime(self)

        if date is not None:
            tmpdate = self.from_datetime(date)
        elif year is not None and month is not None and day is not None:
            tmpdate = tmpdate.replace(year=year, month=month, day=day)
        elif ymd is not None:
            tmpdate = self.from_datetime(self.ymd2date(ymd))
        elif mjd is not None:
            tmpdate = self.from_datetime(self.mjd2date(mjd))

        return tmpdate

    def gpscal(self, *args, date=None, year=None, month=None, day=None, ymd=None, mjd=None):
        """
        return doy, GPS Week (from 1980/1/6), GPS weekday [0:Sunday .. 6:Saturday].

        doy, gpsw, gpswd = gnsstime.gpscal(date = dt.datetime.utcnow())
        doy, gpsw, gpswd = gnsstime.gpscal(year=2013, month=1, day=1)
        doy, gpsw, gpswd = gnsstime.gpscal(ymd="2013/01/01")
        doy, gpsw, gpswd = gnsstime.gpscal(2013, 1, 1)

        example:
        doy, gpsw, gpswd = gnsstime.gpscal(year=2013, month=1, day=1)
        """
        date = self._arg2date(*args, date=date, year=year, month=month,
                            day=day, ymd=ymd, mjd=mjd)
        return date.doy, date.gpsw, date.gpswd

    @property
    def gpsw(self):
        """
        return GPS Week from 1980/1/6.
        """
        return int((self-self.dt_gpst0).days/7.0)

    @property
    def gpswd(self):
        """
        return GPS weekday [0:Sunday .. 6:Saturday].
        """
        return self.isoweekday() % 7 # sunday is 0 for GPS week day, instead of 7.

    @property
    def gpswsec(self):
        """
        return GPS second-of-week.
        """
        return self.gpswd*86400. + self.hour*3600. + self.minute*60. + self.second

    @property
    def weekday(self):
        """
        return Weekday [0:Monday .. 6:Sunday].
        """
        #return date.isoweekday()  # wrong!
        return super(gnsstime, self).weekday()

    @property
    def doy(self):
        """
        return day of the year [1-366].
        """
        return self.timetuple().tm_yday

    @property
    def leapsec(self):
        """
        leap seconds for GPST.
        """
        # search leap seconds
        for leap in self.leaps:
            if self >= leap.date:
                return leap.sec

        # return 0 sec before 1981/7/1
        return 0

    @property
    def gpst(self):
        """
        Return the gps time.
        """
        return self - timedelta(seconds=self.leapsec)

    @property
    def mjd(self):
        """
        Return the modified julian date.
        """
        return (self - DT_MJD0).total_seconds()/86400.

    # timedelta support
    def __add__(self, other):
        """
        Add a gnsstime and timedelta.
        """
        return self.from_datetime(self.to_datetime() + other)

    def __radd__(self, other):
        return self + other

    def __sub__(self, other):
        """
        Subtract a gnsstime and a gnsstime, or a timedelta.
        """
        if isinstance(other, dt.datetime) or isinstance(other, gnsstime):
            dday = self.toordinal() - other.toordinal()
            sec1 = self.hour*3600 + self.minute*60 + self.second
            sec2 = other.hour*3600 + other.minute*60 + other.second
            dsec = sec1 - sec2
            dmicrosec = other.microsecond - other.microsecond
            return dt.timedelta(dday, dsec, dmicrosec)
        elif isinstance(other, dt.timedelta):
            return self + -other

# gpscal for static method
gpscal = gnsstime(1980, 1, 6).gpscal

def test():
    gt = gnsstime(2011, 1, 1)
    print("--- gpscal test ---")
    print("gpscal: show gps calendar (doy, gpsw, gpswd)")
    print("gpscal()                        -> ", gt.gpscal())
    print("gpscal(2013,12,31)              -> ", gt.gpscal(2013, 12, 31))
    print("gpscal(ymd='2013/12/31')        -> ", gt.gpscal(ymd="2013/12/31"))
    print("gpscal(dt.datetime(2013,12,31)) -> ", gt.gpscal(dt.datetime(2013, 12, 31)))
    print("gpscal(year=2013, month=12, day=31) -> ", gt.gpscal(year=2013, month=12, day=31))
    print("gpscal(mjd=56657)               -> ", gt.gpscal(mjd=56657))

    print("--- properties ---")
    print("date    :", gt.isoformat())
    print("doy     :", gt.doy)
    print("gpsw    :", gt.gpsw)
    print("gpswd   :", gt.gpswd)
    print("gpst    :", gt.gpst.isoformat())
    print("leapsec :", gt.leapsec)
    print("mjd     :", gt.mjd)

    print("--- timedelta support ---")
    print("gt :", gt.isoformat())
    print("gt + dt.timedelta(days=2) :", (gt + dt.timedelta(days=2)).isoformat())
    print("gt - dt.timedelta(days=2) :", (gt - dt.timedelta(days=2)).isoformat())

if __name__ == "__main__":
    test()


class Solution:
    def containsDuplicate(self, nums: List[int]) -> bool:

      """
        HIGH LEVEL:
          This problem is pretty straightforward.
          Have a hashmap and store the numbers with their respective frequency.

          Go through the hashmap and check their frequencies.
          If any of the numbers' frequency is > 1:
            return True

          otherwise return False

          Time: O(n)
          Space: O(n)

      """

      if len(nums) == 0:
          return False

      freq = {}

      for num in nums:
          if num not in freq:
              freq[num] = 0
          freq[num] += 1

      for key, value in freq.items():
          if value > 1:
              return True

      return False


import matplotlib.pyplot as plt
from math import log

ys  = []
xss = []

for line in open("in.csv"):
    *xs, y = map(int, line.split())
    xss.append(xs)
    ys.append(y)

xss = [list(x) for x in zip(*xss)] # transpose

for i in range(len(ys)):
    if (ys[i] == 0):
        marker = 'o'
        c = 'r'
    else:
        marker = 'x'
        c = 'b'
    plt.scatter(xss[0][i], xss[1][i], marker=marker, c=c)

plt.show()

def entropy(ys):
    n  = len(ys)
    n0 = len([x for x in ys if x == 0])
    n1 = len([x for x in ys if x == 1])
    return - (n0 / n) * log(n0 / n, 2) - (n1 / n) * log(n1 / n, 2)

class MyData:
    def __init__(self, xss, ys):
        self.xss = xss
        self.ys = ys

    def entropy(self):
        n  = len(self.ys)
        n0 = len([x for x in self.ys if x == 0])
        n1 = len([x for x in self.ys if x == 1])
        return - (n0 / n) * log(n0 / n, 2) - (n1 / n) * log(n1 / n, 2)

    def uniq(self, i):
        return list(set(self.xss[i]))

    def IG(self, i):
        l = self.uniq(i)

        for n in l:
            yl = [self.ys[j] for j, x in enumerate(self.xss[i]) if x == n]
            print(yl)
            print(entropy(yl))

        return yl

m = MyData(xss,ys)