Skip to content

Commit 4d41a71

Browse files
authored
Add notification sending and an alarm when a reminder is triggered (#31)
1 parent fdca476 commit 4d41a71

22 files changed

+607
-24
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,7 @@ make swagger
5151
```
5252

5353
This will open the swagger interface in your browser.
54+
55+
## Credits
56+
57+
Alarm sound from <https://pixabay.com/fr/sound-effects/phone-ringtone-clean-273554/>.

pill_mate/backend/assets/alarm.mp3

751 KB
Binary file not shown.

pill_mate/backend/eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default tseslint.config(
1717
globals: globals.node,
1818
},
1919
rules: {
20+
'no-console': 'error',
2021
'max-len': ['error', { code: 100 }],
2122
indent: ['error', 4],
2223
semi: ['error', 'always'],

pill_mate/backend/src/app.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import express from 'express';
22
import morgan from 'morgan';
33

44
import { createLogger } from './logger';
5+
import homeAssistantRoutes from './routes/homeAssistantRoutes';
56
import medicationRoutes from './routes/medicationRoutes';
67
import reminderRoutes from './routes/reminderRoutes';
78
import userRoutes from './routes/userRoutes';
@@ -19,11 +20,14 @@ app.use(morgan('dev', {
1920
},
2021
}));
2122

23+
app.use('/static', express.static('assets'));
24+
2225
app.use(applicationJson);
2326
app.use(express.json());
2427

2528
app.use(homeAssistantHeaders);
2629

30+
app.use('/homeassistant', homeAssistantRoutes);
2731
app.use('/medication', medicationRoutes);
2832
app.use('/reminder', reminderRoutes);
2933
app.use('/user', userRoutes);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import assert from 'assert';
2+
3+
import { Request, Response } from 'express';
4+
5+
import { HTTP_200_OK } from '../status';
6+
import { asyncErrorHandler } from '../utils';
7+
import { HomeAssistantService } from '../services/HomeAssistantService';
8+
9+
export const getMobileAppDevices = asyncErrorHandler(async (
10+
request: Request,
11+
response: Response,
12+
) => {
13+
assert(request.user !== undefined);
14+
15+
const devies = await HomeAssistantService.getMobileAppDevices();
16+
17+
response
18+
.status(HTTP_200_OK)
19+
.json(devies.map(device => device.name));
20+
});

pill_mate/backend/src/controllers/userController.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
HTTP_404_NOT_FOUND,
1313
} from '../status';
1414
import { asyncErrorHandler, checkUnexpectedKeys } from '../utils';
15+
import { HomeAssistantService } from '../services/HomeAssistantService';
1516

1617
export const me = asyncErrorHandler(async (request: Request, response: Response) => {
1718
assert(request.user !== undefined);
@@ -21,6 +22,7 @@ export const me = asyncErrorHandler(async (request: Request, response: Response)
2122
userName: request.homeAssistantUserName,
2223
userDisplayName: request.homeAssistantUserDisplayName,
2324
role: request.user.role,
25+
mobileAppDevice: request.user.mobileAppDevice,
2426
});
2527
});
2628

@@ -35,15 +37,25 @@ export const me = asyncErrorHandler(async (request: Request, response: Response)
3537
* properties:
3638
* role:
3739
* $ref: '#/components/schemas/UserRole'
40+
* mobileAppDevice:
41+
* type: string
42+
* nullable: true
43+
* description: The devices where notifications will be send.
44+
* example: Redmi Note 8T
3845
*/
3946
type CreateUserBody = {
4047
role: unknown,
48+
mobileAppDevice: unknown,
4149
};
4250

4351
export const createUser = asyncErrorHandler(async (request: Request, response: Response) => {
44-
if (!checkUnexpectedKeys<CreateUserBody>(request.body, ['role'], response)) return;
52+
if (!checkUnexpectedKeys<CreateUserBody>(
53+
request.body,
54+
['role', 'mobileAppDevice'],
55+
response,
56+
)) return;
4557

46-
const { role } = request.body as CreateUserBody;
58+
const { role, mobileAppDevice } = request.body as CreateUserBody;
4759

4860
if (role === undefined) {
4961
response
@@ -59,6 +71,19 @@ export const createUser = asyncErrorHandler(async (request: Request, response: R
5971
return;
6072
}
6173

74+
if ((mobileAppDevice !== undefined &&
75+
mobileAppDevice !== null &&
76+
typeof mobileAppDevice !== 'string') ||
77+
(typeof mobileAppDevice === 'string' &&
78+
(await HomeAssistantService.getMobileAppDevices()).find(
79+
device => device.name === mobileAppDevice,
80+
) === undefined)) {
81+
response
82+
.status(HTTP_400_BAD_REQUEST)
83+
.json({ message: 'Invalid mobileAppDevice.' });
84+
return;
85+
}
86+
6287
const user = await User.findOne({
6388
where: {
6489
homeAssistantUserId: request.homeAssistantUserId,
@@ -75,6 +100,7 @@ export const createUser = asyncErrorHandler(async (request: Request, response: R
75100
const newUser = await User.create({
76101
homeAssistantUserId: request.homeAssistantUserId,
77102
role,
103+
mobileAppDevice,
78104
});
79105

80106
response
@@ -85,6 +111,7 @@ export const createUser = asyncErrorHandler(async (request: Request, response: R
85111
userName: request.homeAssistantUserName,
86112
userDisplayName: request.homeAssistantUserDisplayName,
87113
role: newUser.role,
114+
mobileAppDevice: newUser.mobileAppDevice,
88115
});
89116
});
90117

pill_mate/backend/src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { config } from 'dotenv';
33
config({ path: '../.env' });
44

55
import app from './app';
6-
import { HomeAssistantService } from './services/homeAssistantService';
6+
import { HomeAssistantService } from './services/HomeAssistantService';
77
import { createLogger } from './logger';
8-
import { ReminderService } from './services/reminderService';
8+
import { ReminderService } from './services/ReminderService';
99
import { sequelize } from './sequelize';
1010

1111
const SUPERVISOR_TOKEN = process.env.SUPERVISOR_TOKEN;
@@ -20,9 +20,10 @@ const logger = createLogger('backend');
2020
}
2121

2222
await Promise.all([
23-
sequelize.sync({ alter: { drop: false } }).then(() => ReminderService.initAllTimeouts()),
23+
sequelize.sync({ alter: { drop: false } }),
2424
HomeAssistantService.init(SUPERVISOR_TOKEN),
2525
]);
26+
await ReminderService.initAllTimeouts();
2627

2728
app.listen(PORT, () => {
2829
logger.info(`Server running at http://localhost:${PORT}`);

pill_mate/backend/src/models/Medication.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class Medication extends Model {
5959
declare indication: string | null;
6060

6161
@AllowNull(false)
62-
@Min(0.01)
62+
@Min(0.0)
6363
@Column({
6464
type: DataType.DECIMAL(6, 2),
6565
allowNull: false,

pill_mate/backend/src/models/MedicationUnit.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import assert from 'assert';
12
/**
23
* @openapi
34
* components:
@@ -25,3 +26,20 @@ export enum MedicationUnit {
2526
export const isMedicationUnit = (value: unknown): value is MedicationUnit => {
2627
return typeof value === 'number' && value in MedicationUnit;
2728
};
29+
30+
export const medicationUnitToString = (medicationUnit: MedicationUnit, plural: boolean) => {
31+
switch (medicationUnit) {
32+
case MedicationUnit.TABLET:
33+
return plural ? 'comprimés' : 'comprimé';
34+
case MedicationUnit.PILL:
35+
return plural ? 'pilules' : 'pilule';
36+
case MedicationUnit.ML:
37+
return plural ? 'millilitres' : 'millilitre';
38+
case MedicationUnit.DROPS:
39+
return 'gouttes';
40+
case MedicationUnit.UNIT:
41+
return plural ? 'unités' : 'unité';
42+
default:
43+
assert(false, 'unreachable');
44+
}
45+
};

pill_mate/backend/src/models/Reminder.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import {
2+
BelongsToCreateAssociationMixin,
3+
BelongsToGetAssociationMixin,
4+
BelongsToSetAssociationMixin,
5+
} from 'sequelize';
6+
import {
7+
AfterCreate,
28
AllowNull,
3-
BeforeCreate,
49
BeforeDestroy,
510
BeforeUpdate,
611
BelongsTo,
@@ -12,7 +17,7 @@ import {
1217
Table,
1318
} from 'sequelize-typescript';
1419

15-
import { ReminderService } from '../services/reminderService';
20+
import { ReminderService } from '../services/ReminderService';
1621
import { Medication } from './Medication';
1722
import { User } from './User';
1823

@@ -74,14 +79,18 @@ export class Reminder extends Model {
7479
@Column(DataType.DATEONLY)
7580
declare nextDate: string;
7681

77-
@BelongsTo(() => Medication)
78-
declare medication: Medication;
79-
8082
@ForeignKey(() => Medication)
8183
@AllowNull(false)
8284
@Column
8385
declare medicationId: number;
8486

87+
@BelongsTo(() => Medication)
88+
declare medication: Medication;
89+
90+
declare getMedication: BelongsToGetAssociationMixin<Medication>;
91+
declare setMedication: BelongsToSetAssociationMixin<Medication, number>;
92+
declare createMedication: BelongsToCreateAssociationMixin<Medication>;
93+
8594
@ForeignKey(() => User)
8695
@AllowNull(false)
8796
@Column
@@ -90,6 +99,10 @@ export class Reminder extends Model {
9099
@BelongsTo(() => User)
91100
declare user: User;
92101

102+
declare getUser: BelongsToGetAssociationMixin<User>;
103+
declare setUser: BelongsToSetAssociationMixin<User, number>;
104+
declare createUser: BelongsToCreateAssociationMixin<User>;
105+
93106
/**
94107
* Computes the exact `Date` object when the reminder should trigger.
95108
*
@@ -102,7 +115,7 @@ export class Reminder extends Model {
102115
return nextDate;
103116
}
104117

105-
@BeforeCreate
118+
@AfterCreate
106119
static onCreate(instance: Reminder) {
107120
ReminderService.setUpTimeout(instance);
108121
}

0 commit comments

Comments
 (0)