Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Alarm Modes #1904

Merged
merged 46 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
26a0bf5
Alarm Mode (backend+ test) + part of unlock UI
Pierre-Gilles Sep 29, 2023
10227e2
Add form to update house code/delay
Pierre-Gilles Oct 9, 2023
48923e0
Add API routes to arm/disram
Pierre-Gilles Oct 16, 2023
7a1e2c2
Add front widget to arm/disarm alarm
Pierre-Gilles Oct 16, 2023
454f068
Add a route to get house by selector
Pierre-Gilles Oct 20, 2023
bdc63e5
Add arming status widget
Pierre-Gilles Oct 20, 2023
43fde5d
Add alarm in scenes
Pierre-Gilles Oct 20, 2023
4dfa7b4
Add scene actions/trigger
Pierre-Gilles Oct 20, 2023
a224f5e
Fix tests
Pierre-Gilles Oct 20, 2023
d6220cd
Fix test
Pierre-Gilles Oct 20, 2023
fe9b143
Fix eslint front
Pierre-Gilles Oct 20, 2023
bc2a6f3
Add all tablet logic
Pierre-Gilles Oct 20, 2023
a5541ef
Fix tests
Pierre-Gilles Oct 20, 2023
c4d0587
Add route to update session to tablet mode
Pierre-Gilles Oct 20, 2023
c48f729
Add route to get tablet mode
Pierre-Gilles Oct 20, 2023
12c048e
Add tablet mode
Pierre-Gilles Oct 20, 2023
5852590
Lock/unlock tablet before websocket event
Pierre-Gilles Oct 23, 2023
66478af
Validate token with alarm:write for websockets
Pierre-Gilles Oct 23, 2023
2b162f1
Reduce rate limit
Pierre-Gilles Oct 23, 2023
12a3a54
Unlock alarm
Pierre-Gilles Oct 23, 2023
39301b5
Merge branch 'master' into alarm-mode
Pierre-Gilles Oct 23, 2023
1eb33c5
Fix server tests
Pierre-Gilles Oct 23, 2023
acf3ed1
Should be possible to cancel arming alarm
Pierre-Gilles Oct 23, 2023
25201f5
Improve design of widget
Pierre-Gilles Oct 23, 2023
2845127
Add alarm.arming trigger
Pierre-Gilles Oct 23, 2023
51a2048
Add alarm arming trigger in UI
Pierre-Gilles Oct 23, 2023
a1291bf
Hide tablet mode on Gladys Plus + improve UX tablet mode
Pierre-Gilles Oct 23, 2023
4ad32d8
Add EN translation
Pierre-Gilles Oct 23, 2023
e7733a1
Improve server error status when code in invalid
Pierre-Gilles Oct 23, 2023
fd83f7d
Add error message when code is invalid
Pierre-Gilles Oct 23, 2023
41c1241
Add ability to show password house
Pierre-Gilles Oct 23, 2023
5ce28d8
remove console.log
Pierre-Gilles Oct 26, 2023
f315b60
Add missing tests to errorMiddleware
Pierre-Gilles Oct 26, 2023
a3a7636
Add tests to websocket connection
Pierre-Gilles Oct 26, 2023
eb447f3
Add card around tablet mode
Pierre-Gilles Oct 26, 2023
5de3c02
Add 0 to keypad
Pierre-Gilles Oct 26, 2023
f486805
Fix eslint front
Pierre-Gilles Oct 26, 2023
8f5c259
Fix typo FR
Pierre-Gilles Oct 26, 2023
4eca352
Add alarm set mode action
Pierre-Gilles Oct 26, 2023
f7a78a0
Add more validation on house alarm code
Pierre-Gilles Oct 27, 2023
593a291
Add autocomplete=off and fake password to prevent brower from autocom…
Pierre-Gilles Oct 27, 2023
daa527b
Improve design of locked page + improve robustness
Pierre-Gilles Oct 27, 2023
a2de7bc
Add error message + specific trigger on code failure
Pierre-Gilles Oct 27, 2023
ef36592
Change active buttons behavior
Pierre-Gilles Oct 27, 2023
94a418c
Merge branch 'master' into alarm-mode
Pierre-Gilles Oct 27, 2023
617e3b7
Code should be cleared + keep error message to prevent UI shift
Pierre-Gilles Oct 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions front/src/actions/house.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,30 @@ function createActions(store) {
});
store.setState(newState);
},
updateHouseAlarmCode(state, code, houseIndex) {
const newState = update(state, {
houses: {
[houseIndex]: {
alarm_code: {
$set: code
}
}
}
});
store.setState(newState);
},
updateHouseDelayBeforeArming(state, delayBeforeArming, houseIndex) {
const newState = update(state, {
houses: {
[houseIndex]: {
alarm_delay_before_arming: {
$set: parseInt(delayBeforeArming, 10)
}
}
}
});
store.setState(newState);
},
updateHouseLocation(state, latitude, longitude, houseIndex) {
const newState = update(state, {
houses: {
Expand Down
20 changes: 18 additions & 2 deletions front/src/actions/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ function createActions(store) {
const returnUrl = window.location.pathname + window.location.search;
route(`/login?return_url=${encodeURIComponent(returnUrl)}`);
},
async refreshTabletMode(state) {
try {
const currentSession = await state.httpClient.get('/api/v1/session/tablet_mode');
store.setState({
tabletMode: currentSession.tablet_mode
});
} catch (e) {
console.error(e);
}
},
async checkSession(state) {
if (isUrlInArray(state.currentUrl, OPEN_PAGES)) {
return null;
Expand All @@ -50,7 +60,11 @@ function createActions(store) {
if (!state.session.isConnected()) {
actions.redirectToLogin();
}
const tasks = [state.httpClient.get('/api/v1/me'), actionsProfilePicture.loadProfilePicture(state)];
const tasks = [
state.httpClient.get('/api/v1/me'),
actionsProfilePicture.loadProfilePicture(state),
actions.refreshTabletMode(state)
];
const [user] = await Promise.all(tasks);
store.setState({
user
Expand All @@ -69,7 +83,9 @@ function createActions(store) {
const error = get(e, 'response.data.error');
const gatewayErrorMessage = get(e, 'response.data.error_message');
const errorMessageOtherFormat = get(e, 'response.data.message');
if (status === 401) {
if (status === 401 && errorMessageOtherFormat === 'TABLET_IS_LOCKED') {
route('/locked');
} else if (status === 401) {
state.session.reset();
actions.redirectToLogin();
} else if (error === 'GATEWAY_USER_NOT_LINKED') {
Expand Down
2 changes: 2 additions & 0 deletions front/src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Header from './header';
import Layout from './layout';
import Redirect from './router/Redirect';
import Login from '../routes/login';
import Locked from '../routes/locked';
import Error from '../routes/error';
import ForgotPassword from '../routes/forgot-password';
import ResetPassword from '../routes/reset-password';
Expand Down Expand Up @@ -181,6 +182,7 @@ const AppRouter = connect(
) : (
<ResetPassword path="/reset-password" />
)}
<Locked path="/locked" />
{config.gatewayMode ? <LinkGatewayUser path="/link-gateway-user" /> : <Error type="404" default />}
{config.gatewayMode ? <SignupGateway path="/signup-gateway" /> : <Error type="404" default />}
{config.gatewayMode ? (
Expand Down
199 changes: 199 additions & 0 deletions front/src/components/boxs/alarm/Alarm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { Component } from 'preact';
import { connect } from 'unistore/preact';
import cx from 'classnames';
import { Text } from 'preact-i18n';
import { ALARM_MODES, WEBSOCKET_MESSAGE_TYPES } from '../../../../../server/utils/constants';
import Countdown from './Coutdown';

import style from './style.css';

class AlarmComponent extends Component {
state = {};

arming = async () => {
await this.setState({ arming: true });
};

cancelArming = async () => {
await this.disarm();
await this.getHouse();
};

getHouse = async () => {
await this.setState({ loading: true });
try {
const house = await this.props.httpClient.get(`/api/v1/house/${this.props.box.house}`);
await this.setState({ house, arming: false });
} catch (e) {
console.error(e);
}
await this.setState({ loading: false });
};

callAlarmApi = async action => {
await this.setState({ loading: true });
try {
await this.props.httpClient.post(`/api/v1/house/${this.props.box.house}/${action}`);
} catch (e) {
console.error(e);
}
await this.setState({ loading: false });
};

arm = async () => {
await this.callAlarmApi('arm');
};
disarm = async () => {
await this.callAlarmApi('disarm');
};
partialArm = async () => {
await this.callAlarmApi('partial_arm');
};
panic = async () => {
await this.callAlarmApi('panic');
};

componentDidMount() {
this.getHouse();
this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ALARM.ARMED, this.getHouse);
this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ALARM.ARMING, this.arming);
this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ALARM.DISARMED, this.getHouse);
this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ALARM.PARTIALLY_ARMED, this.getHouse);
this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ALARM.PANIC, this.getHouse);
}

componentWillUnmount() {
this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.ALARM.ARMED, this.getHouse);
this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.ALARM.ARMING, this.getHouse);
this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.ALARM.DISARMED, this.getHouse);
this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.ALARM.PARTIALLY_ARMED, this.getHouse);
this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.ALARM.PANIC, this.getHouse);
}

componentDidUpdate(nextProps) {
const houseChanged = nextProps.box.house !== this.props.box.house;
if (houseChanged) {
this.getHouse();
}
}

render(props, { house, loading, arming }) {
const armingDisabled = (house && house.alarm_mode === ALARM_MODES.ARMED) || arming;
const partialArmDisabled = (house && house.alarm_mode === ALARM_MODES.PARTIALLY_ARMED) || arming;
const isCurrentlyArmingWithCoutdown = arming && house.alarm_delay_before_arming > 0;
return (
<div class="card">
<div class="card-header">
<h3 class="card-title">{props.box.name}</h3>
</div>
{house && (
<div class="card-body">
<div class={loading ? 'dimmer active' : 'dimmer'}>
<div class="loader" />
<div class="dimmer-content">
{!arming && (
<p>
<Text id="dashboard.boxes.alarm.alarmStatusText" />
<b>
<Text id={`alarmModes.${house.alarm_mode}`} />
</b>
.
</p>
)}
{isCurrentlyArmingWithCoutdown && (
<p>
<Text id="dashboard.boxes.alarm.alarmArming" />
<Countdown seconds={house.alarm_delay_before_arming} />
<button class="btn btn-outline-warning btn-block mt-4" onClick={this.cancelArming}>
<Text id="dashboard.boxes.alarm.cancelAlarmArming" />
</button>
</p>
)}
{!isCurrentlyArmingWithCoutdown && (
<div>
<div class="row">
<div class="col-6">
<button
onClick={this.arm}
disabled={armingDisabled}
class={cx('btn btn-block', style.alarmActionButton, {
'btn-outline-primary': house.alarm_mode !== ALARM_MODES.ARMED,
'btn-primary': house.alarm_mode === ALARM_MODES.ARMED
})}
>
<div class="pb-2">
<i class={cx('fe fe-bell', style.alarmActionIcon)} />
</div>
<div>
<Text id="dashboard.boxes.alarm.armButton" />
</div>
</button>
</div>
<div class="col-6">
<button
onClick={this.disarm}
disabled={house.alarm_mode === ALARM_MODES.DISARMED}
class={cx('btn btn-block', style.alarmActionButton, {
'btn-outline-success': house.alarm_mode !== ALARM_MODES.DISARMED,
'btn-success': house.alarm_mode === ALARM_MODES.DISARMED
})}
>
<div class="pb-2">
<i class={cx('fe fe-home', style.alarmActionIcon)} />
</div>
<div>
<Text id="dashboard.boxes.alarm.disarmButton" />
</div>
</button>
</div>
</div>
<div class="row mt-4">
<div class="col-6">
<button
onClick={this.partialArm}
disabled={partialArmDisabled}
class={cx('btn btn-block', style.alarmActionButton, {
'btn-outline-dark': house.alarm_mode !== ALARM_MODES.PARTIALLY_ARMED,
'btn-dark': house.alarm_mode === ALARM_MODES.PARTIALLY_ARMED
})}
>
<div class="pb-2">
<i class={cx('fe fe-shield', style.alarmActionIcon)} />
</div>
<div>
<Text id="dashboard.boxes.alarm.partiallyArmedButton" />
<br />
<Text id="dashboard.boxes.alarm.partiallyArmedButtonSecondLine" />
</div>
</button>
</div>
<div class="col-6">
<button
onClick={this.panic}
disabled={house.alarm_mode === ALARM_MODES.PANIC}
class={cx('btn btn-block', style.alarmActionButton, {
'btn-outline-danger': house.alarm_mode !== ALARM_MODES.PANIC,
'btn-danger': house.alarm_mode === ALARM_MODES.PANIC
})}
>
<div class="pb-2">
<i class={cx('fe fe-alert-circle', style.alarmActionIcon)} />
</div>
<div>
<Text id="dashboard.boxes.alarm.panicButton" />
</div>
</button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}
}

export default connect('httpClient,session', {})(AlarmComponent);
42 changes: 42 additions & 0 deletions front/src/components/boxs/alarm/Coutdown.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { h, Component } from 'preact';
import style from './countdown.css';

class Countdown extends Component {
constructor(props) {
super(props);
this.state = {
seconds: props.seconds
};
}

updateCountdown() {
if (this.state.seconds > 0) {
this.setState(prevState => ({ seconds: prevState.seconds - 1, updated: true }));

// Clear the "updated" class after the animation duration (0.5 seconds)
setTimeout(() => {
this.setState({ updated: false });
}, 500);
}
}

componentDidMount() {
this.countdownInterval = setInterval(this.updateCountdown.bind(this), 1000);
}

componentWillUnmount() {
clearInterval(this.countdownInterval);
}

render() {
return (
<div>
<div class={style.countdown}>
<span class={`${style.countdownTimer} ${this.state.updated ? style.updated : ''}`}>{this.state.seconds}</span>
</div>
</div>
);
}
}

export default Countdown;
Loading
Loading