Choose random instants in a day with a low memory footprint
1. Review


There's a lot of repetition, especially of the number 86400. This can be avoided by defining a global constant, for example:

_DAY_SECONDS = 24 * 60 * 60  # Seconds in a day.


or alternatively:

import datetime
_DAY_SECONDS = datetime.timedelta(1).total_seconds()


and then in mainloop:

day, partday = divmod(int(time.time()), _DAY_SECONDS)
seconds_left = _DAY_SECONDS - partday
if day != planday:
    planday = day
    events_planned = round(getPref('events_per_day') * seconds_left
                           / _DAY_SECONDS)


(This avoids the need for the function planifie.)
If time.time happens to return a number less than 86,400 (perhaps because the computer's clock has not been set correctly) then events_planned will not get set on the first loop. It's better to ensure that the initial value of planday can never equal day, for example by initializing planday to None.


2. The problem

The chance of emitting an event on each loop is events_planned / seconds_left. This would be correct if there each loop iteration took exactly one second. But that's not the case — each loop iteration takes more than a second. That's because (i) some time is taken by do_event (on the iterations where there is an event); (ii) sleep(1) generally takes a little more than 1 second because of the granularity of operating system scheduler, and (iii) as it says in the specification of the sleep function,


  The suspension time may be longer than requested due to the scheduling of other activity by the system.


So each loop takes more than a second, but emits events based on the probability of an event occurring in a period of exactly one second. So there is a small but systematic deficit in the number of emitted events, leading to a rush to catch up at the end.

To have a fair schedule, you need to measure the time actually taken by each iteration of the loop, and adjust the probability accordingly, something like this:

planday = None
prev = time.time()
while not is_closed:
    now = time.time()
    time_passed = now - prev
    day, partday = divmod(now, _DAY_SECONDS)
    seconds_left = _DAY_SECONDS - partday
    if day != planday:
        planday = day
        events_planned = round(getPref('events_per_day') * seconds_left
                               / _DAY_SECONDS)
    if random.random() &lt; events_planned * time_passed / seconds_left:
        events_planned -= 1
        do_event()
    time.sleep(1)


(Note that this doesn't actually ensure that the exact number of events occur in each day — an unlucky scheduling of the last sleep of the day could cause some events to be dropped. But the code in the post has the same problem, and I'm guessing that you don't mind if the number of events per day is short by one or two.)

3. Worked example

You expressed some skepticism about this in the comments, so let's take a simple worked example. Suppose we start out with \$20\$ seconds remaining and \$10\$ events remaining. Then there should be on average one event every 2 seconds. But let's also suppose that the sleep(1) actually takes 2 seconds.

On the first iteration we emit an event with probability \$1\over 2\$, and so after 2 seconds we have emitted on average \$1\over 2\$ an event, so we are 0.5 events behind schedule.

On the second iteration we emit an event with probability \$1\over2\$ if we emitted an event in the first iteration, or \$5\over9\$ if we didn't. So after two iterations we have emitted on average $$ {1\over2}\left(1 + {1\over2}\right) + {1\over2}{5\over9} = {37 \over 36} $$ events, and so we are on average almost 1 event behind schedule.

You can see that in this example, the deficit goes up to start with because although the probability goes up, it doesn't go up fast enough to compensate. Towards the end, the probability catches up, hence the rush to finish.

4. Alternative approach

If the number of events is substantially less than the number of seconds in a day, then most of the loop iterations will be pointless — no event happens and the program just goes to sleep again.

An alternative approach is to emit exactly one event in each loop, and then choose how long to sleep until the next event. If events are distributed uniformly at random, then the time to the next event is given by the exponential distribution and this can be sampled by calling random.expovariate.

(The same problem with over-sleeping will arise here, so you would have to compensate for it, for example by measuring the actual time taken by the loop iteration, subtracting the intended iteration time, and holding over any surplus and subtracting it from the time for the next iteration.)